近期参与的 Web 项目随着代码规模增加,Go 版本升级后,臃肿到不支持跑测试用例。于是做了部分模块功能拆分,尝试避免之前碰到的问题。
我对历史项目中的主要问题简单做了总结,而且会谈谈从哪些方面考虑并改进。
但这不是标准,每个人有每个人的风格和逻辑,只是以我的经验简单记录。
碰到的主要问题
程序高度封装,对配置加载,命令行参数解析包 flag 初始化,routers注册等执行顺序没有控制权
历史项目中,配置初始化、用作命令行解析的 flag 初始化使用第三方库,高度封装,随时随地使用时候调用,这虽然很方便。
但是由于程序运行、单元测试对配置的依赖,自己不能掌控何时加载配置,go testing 模块变动(go 语言设计 testing 的bug),flag.Parse 执行顺序优先于 testing.Init(), 导致执行单个用例测试出现了以下错误:
调整时发现很麻烦,除非重新组织代码,自己控制配置加载。
以上问题暴露的出一个写代码的纠结点,高度封装的项目工程虽然代码量少,使用方便,但灵活性大大降低,给调用者的选择太少了,处理一些特殊问题时很麻烦。
两种方式好坏优劣待定,还是要根据需求去选择。我重写过程中,放弃了高度封装的形式。
日志跟踪不友好
以前对日志处理只是简单的随意按照级别打印或输出到指定位置,用户访问 Web 服务日志无法跟踪集成,因为每个请求到来时,是一个 goroutine, 有自己的 context,多条日志打印或输出顺序不固定,没法对同一个用户操作日志有效过滤筛选。
另外,如果以微服务形式部署应用,免不了要对每个请求的调用链路进行跟踪,常需要将跟踪日志记录集成,常用方法是将跟踪 ID 记录到整个调用链的日志消息中,历史工程这些处理都没有。
针对主要问题做的考虑和改动:
目录结构
项目 目录结构 是代码「可读性、可维护性」体现的属性之一。
一个层次清晰的结构可以让项目接手人一眼看懂,很快了解这个项目。
且随着时间的推移,代码规模的增加,项目结构不会混乱,保持组织良好。
Go 应用程序项目布局虽然没有官方标准,但在 Go 生态中形成了一种比较受欢迎的布局,可以参考:
以下是我的项目布局,目前还没有调整到最优,做一个简单说明。
一些简单说明:
- 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 使用,这些控制整个服务流程,伪代码包括过程如下:
main 函数调用此初始化程序,实现了整个流程控制。
历史工程中充分使用了 go package 的 init 函数,所有路由在 init 函数中初始化,类似: user.go
直接在 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 等信息,可以方便的对跟踪与日志记录集成。
以上。