本文主要介绍笔者建立的一个实用的工程框架。
背景
笔者用 Golang 开发的工程较多,但剥开业务看本质,场景类别并不多:无非是提供 Web 服务、读写数据库、跑一些定时的或一次性的任务。如果每个工程都从零搭建,重复劳动不说,风格还会漂移------今天这个工程日志打终端、明天那个工程又写文件,虽然现在还没有将自己工程移交他人的先例,但自己这么折腾自己,也不是办法。
于是笔者把这些年一个人沉淀下来的代码、习惯固化成一套工程模板:目录结构、配置读取、编译脚本等,都有相同的约定。如果要做新的工程,复制模板改业务即可,框架层基本不动。
本文关注模板的目录结构和功能以及从模板创建新工程的步骤。
设计
目录总览
模板的顶层结构如下(工程名以 foo 代指):
text
foo/
├── 3rdpart/ # 第三方 C 库,按平台分目录归档
├── cmd/ # 应用/模块入口:server、client、misc... 及根命令 root.go
├── common/ # 同包多文件的辅助代码(文件不多时也可直接放根目录)
├── config/ # yaml 配置文件(config.yaml / config_env.yaml / client.yaml)
├── data/ # 静态资源与前端页面(html/css/js/img)
├── internal/ # 内部业务实现,仅本工程可 import
│ ├── conf/ # 配置结构体、加载与热更新
│ ├── server/ # 服务端内部支撑
│ └── client/ # 客户端内部实现
├── log/ # 运行日志目录(运行时生成,按程序名/主机名再分子目录)
├── pkg/ # 通用工具库(日志、加密、数据库封装等,可跨工程复用)
├── vendor/ # go mod vendor 生成的第三方依赖
├── main.go # 程序入口,只调用根命令
├── mybuild.sh # 编译脚本
├── run.sh # 运行脚本(开发用)
├── VERSION # 版本号文件,编译时读取并注入二进制
├── go.mod / go.sum
└── readme.md # 工程说明
各目录职责
| 目录/文件 | 职责 | 备注 |
|---|---|---|
cmd/ |
各"角色"的入口与根命令 | 服务端、客户端、杂项测试都在此,由根命令统一调度 |
internal/ |
业务实现 | Go 编译器强制外部不可引用,天然划分边界 |
pkg/ |
通用工具库 | 与业务无关,理论上可被其它工程复用 |
config/ |
配置文件 | 区分开发/生产环境,程序自动回退 |
data/、db/ |
静态资源与本地数据 | 与代码分离,便于打包发布 |
log/ |
运行日志 | 运行时生成,不入版本库 |
3rdpart/ |
第三方 C 库 | 仅供 cgo 组件使用,纯 Go 工程可去掉 |
VERSION |
版本号 | 编译脚本读取后 -ldflags 注入 |
main.go |
入口 | 只做一件事:执行根命令 |
两条设计约束
- 尽量 pure Go。工程内凡是可选实现的依赖都优先选纯 Go 版本,例如 sqlite 驱动用纯 Go 实现而非依赖 cgo 的版本。这样组件默认可以跨平台编译,只有确有必要(如连接oracle使用的第三方库)才引入 cgo,且把它隔离在特定模块内。
- 编译、配置、运行约定一致 。统一由
mybuild.sh负责编译、VERSION管版本、config/管配置。
实践
从零创建
若从头开始,先初始化模块并拉取依赖:
sh
go mod init foo
go mod tidy
go mod vendor
go mod vendor 值得养成习惯:第三方依赖入库,离线可编、版本可控,编译时加 -mod vendor 即可。
由模板改造
复制模板目录后,只需三步:
- 改模块名 :把
go.mod的 module 名及代码内import路径中的foo批量替换为新工程名; - 删无关模块 :用不到的
cmd/client、3rdpart等整目录删除,多余的第三方依赖go mod tidy自动清理; - 改配置与元信息 :更新
config/下的 yaml、VERSION、readme.md。
框架层的加载、编译、日志无需改动。
编译与运行
模板在根目录提供统一入口,任何平台都执行同一命令:
sh
sh mybuild.sh # 编当前平台
sh mybuild.sh linux arm # 指定平台,如果不涉及cgo,可以在Windows上执行
实例:
$ sh mybuild.sh
build for platform: windows amd64 version: 0.0.1 output file: foobar-win.exe
$ sh mybuild.sh linux
cross build under windows
build for platform: linux amd64 version: 0.0.1 output file: foobar
$ sh mybuild.sh linux arm
not support arm, use arm64
cross build under windows
build for platform: linux arm64 version: 0.0.1 output file: foobar-arm
通过名称能快速辨别出其运行的目标机器平台。
开发期则用 run.sh 完成"编译 + 运行",免去手工切来切去。日志统一落到 log/ 下,按 程序名_主机名 分子目录,多实例部署互不干扰。
值得说明的是,本工程设置的config目录存放配置文件,能区分开发测试环境和生产环境(由代码自动检查判断config.yaml 和 config_env.yaml)。
技术栈
这里记录一些工程中使用的技术线,如下:
- 命令行参数:使用
github.com/spf13/cobra实现命令行参数读取,大大扩大程序功能范围。通过参数,可在同一工程既实现业务功能,又可支持测试程序。 - 参数配置:使用
github.com/spf13/viper和github.com/fsnotify/fsnotify支持yaml格式参数的加载和热更新,减少参数修改导致重启程序次数。 - 数据库:使用
github.com/jmoiron/sqlx提升查询语句开发效率(注:不使用xorm相关库)。需要说明的是,对于常用的sqlite数据库类型,本工程使用modernc.org/sqlite,而不是github.com/mattn/go-sqlite3,因为前者为pure go实现,可跨平台编译使用,就功能而言未发现其与后者有较明显差异,因此使用之。 - web功能:使用
github.com/gin-gonic/gin实现web接口,使用内嵌的html模板实现前端页面(注:暂不使用vue等流行框架)。实现系统信息查询、版本号查询等常用接口。
小结
上述框架目录结构清晰,分工明确,具备基础功能,比如:目录职责明确、配置与代码分离、构建信息统一注入、入口统一调度。当然模板不是一成不变的,由于笔者不断使用和更新,随着实践的深入,功能也将变得丰富,后续根据实际情况,会抽取一些功能点单独作为文章。