在 Go 项目开发中,开发场景最多的是开发一个 Web 服务器,Web 服务器种类有很多,例如:HTTP 服务器、RPC 服务器、WebSocket 服务器等。

其中,HTTP 服务器是最常需要开发的服务器类型。HTTP 服务器,其实就是一个对外提供 API 接口的 Web 服务器。API 接口其实是有规范的,当前用的最多的 REST 规范。

所以,本节课,就来看下如何快速开发一个 REST API 服务器。

REST Web 框架选择

要编写一个 RESTful 风格的 API 服务器,你可以自己调用 net/http包手动实现一个,但这样耗时而且效果也不好。所以企业开发中,通常会使用 Web 框架来开发一个 REST 服务器。

Web 框架有很多,例如:GinHertzEchoEiber 等。当前使用最多的是 Gin 框架。Gin 框架很轻量,并且具有以下优点:高性能、扩展性强、稳定性强、相对而言比较简洁(查看 性能对比)。关于 Gin 的更多介绍可以参考 Gin 官网。

开发一个简单的 REST 服务器

使用 Gin 框架开发一个 REST 服务器分为以下几步:

  1. 配置 REST 服务器(配置监听端口);
  2. 创建 Gin 引擎;
  3. 设置 Gin 路由;
  4. 启动 Gin 服务器。

步骤 1:配置 REST 服务器

修改 cmd/fg-apiserver/app/options/options.go 文件,给 ServerOptions 结构体添加 Addr 配置项。代码变更如下:

package options

import (
    "fmt"
    "net"
    "strconv"
    ...
)

type ServerOptions struct {
    ...
    Addr         string                       `json:"addr" mapstructure:"addr"`
}

// NewServerOptions 创建带有默认值的 ServerOptions 实例.
func NewServerOptions() *ServerOptions {
    return &ServerOptions{
        ...
        Addr:         "0.0.0.0:6666",
    }
}

// Validate 校验 ServerOptions 中的选项是否合法.
// 提示:Validate 方法中的具体校验逻辑可以由 Claude、DeepSeek、GPT 等 LLM 自动生成。
func (o *ServerOptions) Validate() error {
    ...
    // 验证服务器地址
    if o.Addr == "" {
        return fmt.Errorf("server address cannot be empty")
    }

    // 检查地址格式是否为host:port
    _, portStr, err := net.SplitHostPort(o.Addr)
    if err != nil {
        return fmt.Errorf("invalid server address format '%s': %w", o.Addr, err)
    }

    // 验证端口是否为数字且在有效范围内
    port, err := strconv.Atoi(portStr)
    if err != nil || port < 1 || port > 65535 {
        return fmt.Errorf("invalid server port: %s", portStr)
    }

    return nil
}

// Config 基于 ServerOptions 构建 apiserver.Config.
func (o *ServerOptions) Config() (*apiserver.Config, error) {
    return &apiserver.Config{
        ...
        Addr:         o.Addr,
    }, nil
}

上面的代码给 ServerOptions 结构体添加了 Addr 字段,用来保存 Web 服务器的监听地址,默认地址设置为:0.0.0.0:6666

Validate方法中,添加了对 Addr 字段的校验,检查了 Addr字段的格式是否合法,端口的范围是否正确。在实际开发中,你可以根据实际需要添加更多的校验。

Config 方法中,需要将应用的 Addr 配置字段赋值给运行时配置。所以还要在运行时配置中添加 Addr 字段。修改 internal/apiserver/server.go 文件中的 Config 结构体添加 Addr 字段:

type Config struct {
    ...
    Addr         string
}

步骤 2:创建 Gin 引擎

修改 internal/apiserver/server.go 文件,在 Server 结构体中新增 *http.Server 类型的字段 srv,并在 NewServer方法中实例化 srv,变更代码如代码清单 10-1 所示:

package apiserver

import (
    ...
    "net/http"
    ...
    "github.com/gin-gonic/gin"
    ...
)
...
// Server 定义一个服务器结构体类型.
type Server struct {
    ...
    srv *http.Server
}

// NewServer 根据配置创建服务器.
func (cfg *Config) NewServer() (*Server, error) {
    // 创建 Gin 引擎
    engine := gin.New()

    // 注册 404 Handler.
    engine.NoRoute(func(c *gin.Context) {
        c.JSON(http.StatusNotFound, gin.H{"code": "PageNotFound", "message": "Page not found."})
    })

    // 注册 /healthz handler.
    engine.GET("/healthz", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // 创建 HTTP Server 实例
    httpsrv := &http.Server{Addr: cfg.Addr, Handler: engine}

    return &Server{cfg: cfg, srv: httpsrv}, nil
}

NewServer 方法中,通过 gin.New() 创建了一个 Gin 引擎实例 engine。并通过 engine 中的方法给 engine 设置了 REST 路由和中间件。

代码中的 engine.NoRoute 方法调用,设置了当 Gin 找不到匹配的请求路径时返回的信息:

{"code":"PageNotFound","message":"Page not found."}

NewServer 方法的最后部分,创建了标准库的 http.Server 实例,并将配置好的 Gin 引擎设置为其Handler,随后将配置对象和 HTTP 服务器实例注入到新创建的 Server 结构体中并返回。

步骤 3:设置 Gin 路由

有时候服务器进程起来不代表服务器可以正常对外提供 API,我就曾经就遇到过这种问题:服务器进程存在,但是访问 API 确实失败的。

因此在启动服务器时,最好加一个健康检查接口。可以在健康检查接口中,检查任何我们觉得会影响服务器健康状态的项目。

在代码清单 10-1 中,通过以下方法调用给 engine 实例添加了一条健康检查 HTTP 路由:

    // 注册 /healthz handler.
    engine.GET("/healthz", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

engine 实例提供了 GETPOSTDELETEPATCHPUTOPTIONSHEADAny 方法,来给 engine 实例添加对应的 HTTP 路由。

上述代码添加了一条 HTTP 路由:

  • 请求方法:GET
  • 请求路径:/healthz
  • 请求返回:{"status":"ok"}

步骤 4:启动 Gin 服务器

NewServer 方法中创建了 Server 类型的实例,接下来就可以在 Server 实例的 Run 方法中启动 HTTP 服务器。代码如下:

// Run 运行应用.
func (s *Server) Run() error {
    // 运行 HTTP 服务器
    // 打印一条日志,用来提示 HTTP 服务已经起来,方便排障
    slog.Info("Start to listening the incoming requests on http address", "addr", s.cfg.Addr)
    if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        return err
    }

    return nil
}

上述代码,打印了一条日志,日志中输出了 HTTP 服务器的请求端口。将请求端口打印出来,可以用来帮助开发者了解 HTTP 的启动配置。

s.srv.ListenAndServe() 方法用来启动 HTTP 服务器。当该方法返回错误时,报错并退出。这里要注意,上述代码处理了一个特殊的错误情况:http.ErrServerClosed。当我们主动调用 http.ServerShutdown()Close() 方法时,ListenAndServe() 会返回这个特定的错误(http.ErrServerClosed),这表示服务器是被预期地、主动地关闭,而非意外崩溃。因此,这种情况不应被视作异常或错误,不需要额外处理或返回。

编译并运行

执行以下命令编译并运行:

$ ./build.sh
$ _output/fg-apiserver -c configs/fg-apiserver.yaml

打开一个新的 Linux 终端,执行以下命令测试功能是否正常:

$ curl http://127.0.0.1:6666/nonexist # 请求路径不存在时,返回预期错误
{"code":"PageNotFound","message":"Page not found."}
$ curl http://127.0.0.1:6666/healthz # 请求健康检查接口,返回服务器 ok 信息
{"status":"ok"}

附录: cURL 工具介绍

本节课采用 cURL 工具来测试 RESTful API,标准的 Linux 发行版都安装了 cURL 工具。cURL 可以很方便地完成对 REST API 的调用场景,比如:设置 Header,指定 HTTP 请求方法,指定 HTTP 消息体,指定权限认证信息等。

通过 -v 选项也能输出 REST 请求的所有返回信息。cURL 功能很强大,有很多参数,这里列出 REST 测试常用的参数:

-X/--request [GET|POST|PUT|DELETE|]  指定请求的 HTTP 方法
-H/--header                           指定请求的 HTTP Header
-d/--data                             指定请求的 HTTP 消息体(Body)
-v/--verbose                          输出详细的返回信息
-u/--user                             指定账号、密码
-b/--cookie                           读取 cookie
03-13 11:25