暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一点 Go Web 编程实践经验

少数人知道的秘密 2020-09-12
592
近期参与的 Web 项目随着代码规模增加,Go 版本升级后,臃肿到不支持跑测试用例。于是做了部分模块功能拆分,尝试避免之前碰到的问题。
我对历史项目中的主要问题简单做了总结,而且会谈谈从哪些方面考虑并改进。
但这不是标准,每个人有每个人的风格和逻辑,只是以我的经验简单记录。

碰到的主要问题

程序高度封装,对配置加载,命令行参数解析包 flag 初始化,routers注册等执行顺序没有控制权

历史项目中,配置初始化、用作命令行解析的 flag 初始化使用第三方库,高度封装,随时随地使用时候调用,这虽然很方便。
但是由于程序运行、单元测试对配置的依赖,自己不能掌控何时加载配置,go testing 模块变动(go 语言设计 testing 的bug),flag.Parse 执行顺序优先于 testing.Init(), 导致执行单个用例测试出现了以下错误:
 https://github.com/golang/go/issues/31859
调整时发现很麻烦,除非重新组织代码,自己控制配置加载。
以上问题暴露的出一个写代码的纠结点,高度封装的项目工程虽然代码量少,使用方便,但灵活性大大降低,给调用者的选择太少了,处理一些特殊问题时很麻烦。
两种方式好坏优劣待定,还是要根据需求去选择。我重写过程中,放弃了高度封装的形式。

日志跟踪不友好

以前对日志处理只是简单的随意按照级别打印或输出到指定位置,用户访问 Web 服务日志无法跟踪集成,因为每个请求到来时,是一个 goroutine, 有自己的 context,多条日志打印或输出顺序不固定,没法对同一个用户操作日志有效过滤筛选。
另外,如果以微服务形式部署应用,免不了要对每个请求的调用链路进行跟踪,常需要将跟踪日志记录集成,常用方法是将跟踪 ID 记录到整个调用链的日志消息中,历史工程这些处理都没有。
针对主要问题做的考虑和改动:

目录结构

项目 目录结构 是代码「可读性、可维护性」体现的属性之一。
一个层次清晰的结构可以让项目接手人一眼看懂,很快了解这个项目。
且随着时间的推移,代码规模的增加,项目结构不会混乱,保持组织良好。
Go 应用程序项目布局虽然没有官方标准,但在 Go 生态中形成了一种比较受欢迎的布局,可以参考:
https://github.com/golang-standards/project-layout/blob/master/README_zh.md
以下是我的项目布局,目前还没有调整到最优,做一个简单说明。 
一些简单说明:
  • api
该目录表示项目提供api。内部对 api 分类,分为 http 接口和 grpc 接口,可以根据需求增加或调整。
http 接口中的目录介绍会在 web 服务代码结构中说明。
  • cmd
项目中应用的可执行文件,各应用入口。/cmd/http 表示 http 接口入口。
  • internal
私有应用程序代码,实现我项目中的一些业务单元核心功能函数,这部分代码不希望其他人在其应用程序或库中导入
  • pkg
外部应用程序可以使用的库代码,其他项目也可导入共同使用。
  • deployments
PaaS 系统容器编排部署配置和模板。develop 表示测试环境模板,prod 是生产环境模板。
  • docs
存放项目文档。

Web 服务代码结构

主要在 api/http 下: 


  • app.go
http 服务主要代码,提供给 main 使用,这些控制整个服务流程,伪代码包括过程如下:
// Init 应用初始化
func Init(ctx context.Context, opts ...Option) func() {
加载配置
err := config.LoadGlobalConfig(o.ConfigFile)
...
cfg := config.GetGlobalConfig()

初始化日志
loggerCall, err := InitLogger()
handleError(err)

初始化基础服务提供
initService()
srv := &http.Server{
Addr: addr,
Handler: InitWeb(),
...
}

go func() {
logger.Printf(ctx, "HTTP service start,listening address:[%s]", addr)
err := srv.ListenAndServe()
...
}()
}
//InitWeb 初始化web引擎
func InitWeb() *gin.Engine {

app := gin.New()
app.NoMethod(middleware.NoMethodHandler())
app.NoRoute(middleware.NoRouteHandler())

崩溃恢复
app.Use(middleware.Recovery())

权限校验
app.Use(middleware.auth())

设置跟踪ID
app.Use(middleware.Trace()

访问日志
app.Use(middleware.Logger()

跨域请求
if cfg.CORS.Enable {
app.Use(middleware.CORS())
}

// 注册/api路由
registerRouter(app)

return app
}

main 函数调用此初始化程序,实现了整个流程控制。
历史工程中充分使用了 go package 的 init 函数,所有路由在 init 函数中初始化,类似:user.go
func init(){
r.Get("/users/:id",func (c *gin.Context) {

})
}

直接在 main 函数中使用 “_” 引入该包,会先调用包中的初始化函数,这种使用方式让导入的包直接初始化,没有在程序中自己控制流程。
  • config
配置加载,初始化等封装。
  • context
跟踪 id 上下文,用户 id 上下文创建等操作。
  • global
全局使用对象从这获取。
  • middleware
    - auth: 授权中间件
    - cros: 跨域请求中间件
    - limit: api 请求频率限制
    – logger: 日志中间件
    – recover: 崩溃恢复中间件
    – tract: 跟踪 id 设置中间件


  • routers
接口路由。
  • service
接口需要的操作函数封装,调用 internal 中的服务。

日志

通过 contex 跟踪,日志中附加了 「跟踪 id」,「访问用户 user_id」。
大概日志是这样的: 


这样日志中携带了 trace_id,user_id 等信息,可以方便的对跟踪与日志记录集成。
以上。
文章转载自少数人知道的秘密,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论