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微服务框架: 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
}
-
将框架配置的环境值设置成 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 层使用, 这个方法比较全能可以丢给它任意数据。
-
data 为 nil 时,说明只要响应,不需要响应数据,(比如更新,删除)
-
data 为 status.Status 类型时,(status.Status 类型后面章节会介绍),它实现了 error 接口,可以解析错误业务错误码。
-
data 为 error 时,可能是后台bug 或者内部网络问题,中间件出错,这时需要人工介入。
-
其他就是正常数据返回。
参数校验和错误码设计,篇幅原因后面再展开介绍。
欢迎留言讨论,点赞👍 分享