写点什么

一点 Go Web 编程实践经验

用户头像
Garfield
关注
发布于: 2020 年 09 月 12 日
一点 Go Web 编程实践经验

​近期参与的 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 等信息,可以方便的对跟踪与日志记录集成。



以上。

----------------------

公众号:life-is-x

知乎:OneZero



发布于: 2020 年 09 月 12 日阅读数: 46
用户头像

Garfield

关注

Golang Service Mesh 2018.05.15 加入

还未添加个人简介

评论

发布
暂无评论
一点 Go Web 编程实践经验