7、基于Gin实现HTTP服务器

在go项目开发中,开发场景很多的是开发一个web服务器、web服务器种类有很多,例如:Http服务器、RPC服务器、webSocket服务器等。

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

REST Web框架选择

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

Web框架有很多,例如:GinHertzEchoEiber等。用的最多的是Gin,查看性能对比

开发一个简单的REST服务器

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

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

修改cmd/fg-apiserver/app/options/options.go 文件,给ServerOptions结构体添加Addr配置项。

package options

import (
	"fmt"
	"gitlab.com/onexstack/fastgo/internal/apiserver"
	genericoptions "gitlab.com/onexstack/fastgo/pkg/options"
	"log/slog"
	"net"
	"strconv"
)

type ServerOptions struct {
	MySQLOptions *genericoptions.MySQLOption `json:"mysql" mapstructure:"mysql"`
	Addr         string                      `json:"addr" mapstructure:"addr"`
}

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

// Validate
//
//	@Description: Validate校验ServierOptions参数是否合法
//	@receiver s
//	@return error
func (s *ServerOptions) Validate() error {
	// 验证服务器地址
	if s.Addr == "" {
		slog.Error(fmt.Sprintf("server address cannot be empty"))
		return fmt.Errorf("server address cannot be empty")
	}
	// 检查地址是否为host:port
	_, portStr, err := net.SplitHostPort(s.Addr)
	if err != nil {
		slog.Error(fmt.Sprintf("invalid server address format '%s': %w", s.Addr, err))
		return fmt.Errorf("invalid server address format '%s': %w", s.Addr, err)
	}
	// 验证端口是否为数字且在有效范围内
	port, err := strconv.Atoi(portStr)
	if err != nil || port < 1 || port > 65535 {
		return fmt.Errorf("invalid server port: %s", portStr)
	}
	// 校验mysql格式
	mysqlErr := s.MySQLOptions.Validate()
	if mysqlErr != nil {
		slog.Error(mysqlErr.Error())
		return mysqlErr
	}
	return nil
}

// Config
//
//	@Description: 基于ServerOptions构建apiserver.Config
//	@receiver s
//	@return *apiserver.Config
//	@return error
func (s *ServerOptions) Config() (*apiserver.Config, error) {
	return &apiserver.Config{
		MYSQLOptions: s.MySQLOptions,
		Addr:         s.Addr,
	}, nil
}

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

在Validate方法中,添加了对Addr字段的校验。

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

type Config struct {
	MYSQLOptions *genericoptions.MySQLOption
	Addr         string
}
步骤二:创建Gin引擎

修改internal/apiserver/server.go,在Server结构体中增加*http.Server 类型的字段srv,并在NewServer方法中实例化srv

// Server
// @Description: 定义一个服务器结构体类型
type Server struct {
	cfg *Config
	srv *http.Server
}
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":    404,
			"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":404,"message":"Page not Found"}

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

步骤三:设置Gin路由

有时候服务器进程起来不代表服务器可以正常对外提供API,因此在启动服务器时最好加一个健康检查接口。

// 注册/healthz handler
engine.GET("/healthz", func(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
	})
})
步骤四:启动Gin服务器

NewServer方法中创建了Server的实例,接下来就可以在Server实例的Run方法中启动HTTP服务器

func (s *Server) Run() error {
	slog.Info(fmt.Sprintf("Read MYSQL host from config %s", s.cfg.MYSQLOptions.Addr))
	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

}

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

编译并运行

(base) dujie@MacBook-Pro fastgo % sh build.sh                                        
gitlab.com/onexstack/fastgo/internal/apiserver
gitlab.com/onexstack/fastgo/cmd/fg-apiserver/app/options
command-line-arguments
^[[A%                                                                                                                                                                                                                                                                                                                   (base) dujie@MacBook-Pro fastgo % ./_output/fg-apiserver -c configs/fg-apiserver.yaml
/Users/dujie/.fastgo/fg-apiserver.yaml
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /healthz                  --> gitlab.com/onexstack/fastgo/internal/apiserver.(*Config).NewServer.func2 (1 handlers)

打开一个新的Linux终端测试

(base) dujie@MacBook-Pro fastgo % curl http://127.0.0.1:6666/nonexist
{"code":404,"message":"Page not Found"}%                                                                  base) dujie@MacBook-Pro fastgo % curl http://127.0.0.1:6666/healthz 
{"status":"ok"}%               

curl工具常用参数

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