从零构建Go微服务框架: 5.Gin实现的Http服务

Web 服务的核心,处理请求响应的入口。

仓库🌟: https://github.com/bobacgo/kit

涉及文件:

.
├── LICENSE
├── README.md
├── app
│   ├── http.go
│   ├── server
│   │   ├── http
│   │   │   └── middleware
│   │   │       ├── logger_response_fail.go
│   │   │       ├── recovery.go
│   │   │       └── resp_writer.go
│   ├── service.go
└── web
    └── r
        ├── response.go
        └── status
            └── status.go

 

回顾

        从零构建Go微服务框架: 2.服务配置模块设计

        从零构建Go微服务框架: 3.日志模块设计

        从零构建Go微服务框架: 4. Server组件统一接口设计

      前面几章是一个微服务必要的功能,后面章节的是在微服务开发中急需的模块。接下来我们先从 http server 开始,实现 Server 接口之一。

 那么 HttpServer 应该如何设计?🤔

  • 路由树。

  • 解析请求参数。

  • 请求体参数校验。

  • 响应体封装(业务状态码设计)

  • 中间件(数据压缩、请求错误日志、异常捕获、限流、鉴权等)。

  • 优雅退出。

  • 健康检查接口。

  • pprof 接口。

  • API 文档。

我们选择了go 热门的 gin web框架封装,用于提供web服务必要的功能。

gin 内部使用(Radix Tree)前缀树算法来匹配请求API路径.。

  • 支持动态路由(/:param)和 通配符路由(/*filepath)

  • 路由分组。

👉🏻 我们来看对 gin 的封装

- Start() 的实现

func (srv *HttpServer) Start(ctx context.Context) error {
    cfg := srv.Opts.Conf()
// 1.
    switch cfg.Env {
    case enum.EnvProd:
        gin.SetMode(gin.ReleaseMode)
    case enum.EnvDev:
        gin.SetMode(gin.DebugMode)
    case enum.EnvTest:
        gin.SetMode(gin.TestMode)
    }
// 2.
    e := gin.New()
    e.ContextWithFallback = true // 兼容 gin.Context.Get()
// 3.    
    e.Use(gin.Logger())
    e.Use(middleware.Recovery())
    e.Use(middleware.LoggerResponseFail())
    if strings.EqualFold(string(cfg.Env), string(enum.EnvDev)) {
        slog.Warn(fmt.Sprintf(`[gin] Running in "%s" mode`, gin.Mode()))
    }
// 4.
    srv.healthApi(e, cfg) // provide health API
    srv.swaggerApi(e)     // provide swagger API
    srv.pprofApi(e)       // provide pprof API
// 5.
    if srv.RegistryFn != nil {
        srv.RegistryFn(e, srv.Opts) // register router
    }
// 6. 保证端口监听成功
    listen, err := net.Listen("tcp", cfg.Server.Http.Addr)
    if err != nil {
        return err
    }
    srv.server = &http.Server{Handler: e}
    localhost, _ := getRegistryUrl("http", cfg.Server.Http.Addr)
    slog.Info("http server running " + localhost)
    slog.Info("API docs " + localhost + "/swagger/index.html")
// 7.    
    go func(lit net.Listener) {
        if err := srv.server.Serve(lit); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Panicf("listen: %s\n", err)
        }
    }(listen)
    return nil
}
  1. 将框架配置的环境值设置成 gin 对应的mode, 也分三种mode

模式

适用场景

日志级别

调试信息

性能优化

debug

开发

debug

✅ 启用

❌ 关闭

release

生产

error

❌ 关闭

✅ 启用

test

测试

debug

✅ 启用

❌ 关闭

针对 gin 框架来说

  • debug 模式:适用于开发环境,提供详细的日志,方便调试。

  • test 模式: 适用于测试环境,与 debug 模式类型,但有一些特殊行为。

  • release 模式: 使用于生产环境,屏蔽调试日志,提高性能。

    • 日志级别 error,不会打印访问日志(仅保留严重错误日志)

    • 关闭调试信息减少CPU消化和日志存储压力。

    • 启用 Gzip 压缩提升API响应效率。

2.  创建 gin 对象,并开启 ContextWithFallback = true,gin 会优先使用 Requset.Context(), 好处是把可以直接把 gin.Context 透传到 service、repo 层,并能够直接获取 kv 值。

3. e.Use() 注入了三个中间件

- gin.Logger() 输出 api 列表,并会打印请求到响应的耗时.

 middleware.Recovery() 如果程序异常 panic 我们会把捕获到的异常信息响应回去。在后台也会打印对应的堆栈信息。

func Recovery() func(c *gin.Context) {
    return gin.CustomRecovery(func(c *gin.Context, err any) {
        if errMsg, ok := err.(string); ok {
            err = errors.New(errMsg)
        }
        r.Reply(c, err)
    })
}

发生panic时的堆栈信息

middleware.LoggerResponseFail() 对响应体进行拦截,如果响应体是 "application/json" 类型并且业务状态码不是 codes.OK 就把日志记录下来。

    4. 提供了 healthApi、swaggerApi、pprofApi

    • healthApi: 当服务数量很多时,各个服务都有自己不同的业接口,所以提供一个简易通用的接口,检测服务是否能正常的提供API访问。也可以用来做 kubernetes 对该 pod 的探针。

     

    • swaggerApi: http 服务接口一般是以 json 交互,所以需要文档来说明接口和参数。

    • 需要安装 

      go install github.com/swaggo/swag 

      用于构建 swagger 文档。

    在 main 函数上添加生成命令:

    //go:generate swag init --parseDependency --parseInternal --dir ./ --output ./docs
      访问地址:
      
      http://localhost:8080/swagger/index.html
      

       注意:main.go 文件需要把编译后 docs 目录需要引入

      _ "github.com/bobacgo/kit/examples/docs"
      

      pprofApi: 可以对当前应用分析性

      func pprofApi(e *gin.Engine) {
          // 添加 pprof 路由
          e.GET("/debug/pprof/", gin.WrapF(pprof.Index))
          e.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
          e.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
          e.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
          e.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
          e.GET("/debug/pprof/allocs", gin.WrapF(pprof.Handler("allocs").ServeHTTP))
          e.GET("/debug/pprof/block", gin.WrapF(pprof.Handler("block").ServeHTTP))
          e.GET("/debug/pprof/goroutine", gin.WrapF(pprof.Handler("goroutine").ServeHTTP))
          e.GET("/debug/pprof/heap", gin.WrapF(pprof.Handler("heap").ServeHTTP))
          e.GET("/debug/pprof/mutex", gin.WrapF(pprof.Handler("mutex").ServeHTTP))
          e.GET("/debug/pprof/threadcreate", gin.WrapF(pprof.Handler("threadcreate").ServeHTTP))
      }

        执行

        go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap

        可以看到内存占用分析。

         

        注意:浏览器没有显示可能需要安装对应的工具 

        mac系统  brew install graphviz

        5. 然后注册路由,给业务服务提供回调方法,编写对应的api handler 和路由。

        6. net.Listen() 保证能够监听端口。

        7. srv.Server() 会阻塞当前线程,使用一个协程把 http server 运行起来。

        - Stop() 的实现

        
        if srv.server == nil {
            return nil
        }
        slog.Info("Shutting down http server...")
        return srv.server.Shutdown(ctx)

        执行 Shutdown() 优雅退出服务,尽可能的保证业务处理完成前退出。

        👉🏻 拦截器(中间件的使用)

        // 模拟鉴权拦截器
        func Auth() gin.HandlerFunc {
            return func(c *gin.Context) {
                auth := c.GetHeader("Authorization")
                if auth != "" {
                    c.Abort()
                }
                c.Next()
            }
        }

        使用通过 gin.Use()

        func Register(e *gin.Engine, app *app.Options) {
            r := e.Group("/")
            r.Use(middleware.Auth()) // 使用鉴权中间件
            admin.Register(r, app)
            // ....
        }

        👉🏻定义路由

        func Register(r *gin.RouterGroup, app *app.Options) {
            userHandler := handler.NewUserHandler()
            // sys user
            r.POST("v1/user/create", userHandler.Create)
            r.PUT("v1/user/update", userHandler.Update)
            r.DELETE("v1/user/delete", userHandler.Delete)
            r.POST("v1/user/pageList", userHandler.PageList)
            // ....
        }

        👉🏻外层响应体结构定义 web/r/response.go 

        type Response[T any] struct {
            Code codes.Code `json:"code"`
            Data T          `json:"data"`
            Msg  string     `json:"message"`
            Err  any        `json:"err,omitempty"`
        }
        • Code int 的类型。

        • Data 就是要返回的数据。

        • Msg 提示信息,可以用于用户提示。

        • Err 错误信息方便排查问题,主要隐私安全。

        gin 响应时需要指定文本协议和http状态码。

        前后端一般都是json交互,并且接口http响应是正常的,所以我们提供一个默认的方法(json + 200)

        func Reply(c *gin.Context, data any) {
            httpCode := http.StatusOK
            resp := Response[any]{Code: codes.OK, Data: struct{}{}}
            switch v := data.(type) {
            case nil:
            case *status.Status:
                //httpCode = codesToHttpCode(s.Code)
                resp.Code = v.GetCode()
                resp.Msg = v.GetMessage()
                if v.Details != nil {
                    resp.Err = detailErrorType(c, v.Details)
                }
            case error:
                //httpCode = http.StatusInternalServerError
                resp.Code = errs.InternalError.Code
                resp.Msg = errs.InternalError.Message
                slog.ErrorContext(c, v.Error())
            default:
                resp.Data = data
            }
            c.JSON(httpCode, resp)
        }

        提供一个 Reply() 方法, 在 handler 层使用, 这个方法比较全能可以丢给它任意数据。

        1. data 为 nil 时,说明只要响应,不需要响应数据,(比如更新,删除)

        2. data 为 status.Status 类型时,(status.Status 类型后面章节会介绍),它实现了 error 接口,可以解析错误业务错误码。

        3. data 为 error 时,可能是后台bug 或者内部网络问题,中间件出错,这时需要人工介入。

        4. 其他就是正常数据返回。

        参数校验和错误码设计,篇幅原因后面再展开介绍。

        欢迎留言讨论,点赞👍 分享

        图片

         

          评论
          添加红包

          请填写红包祝福语或标题

          红包个数最小为10个

          红包金额最低5元

          当前余额3.43前往充值 >
          需支付:10.00
          成就一亿技术人!
          领取后你会自动成为博主和红包主的粉丝 规则
          hope_wisdom
          发出的红包
          实付
          使用余额支付
          点击重新获取
          扫码支付
          钱包余额 0

          抵扣说明:

          1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
          2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

          余额充值