Go 项目最佳实践

最近 2 个月断断续续用 Go 实现了一个发布系统,主要特性包括:

  • 主机管理
  • 应用管理
  • 服务的部署和发布

这是第一次使用 Go 实现复杂系统,记录一下最佳实践。

库和框架使用

更接近 Go 风格的简单 web framework

web 框架用的是 gorilla 的一套:

  • github.com/gorilla/context
  • github.com/gorilla/handlers
  • github.com/gorilla/mux
  • github.com/gorilla/websocket

在用的过程中还发现了 mux 的一个问题,见Middleware does not work because of subrouter order,这个问题根本原因就是遍历 route 时一旦发现有有不匹配的情况,后续 route 就不会在遍历了,这个问题也在打包前端 app 的时候出现过比较诡异语法错误的现象,原因就是配置的 NotFoundHandler 路由拦截了前端 js 文件的加载,因此配置 NotFoundHandler 要在 router 的最后进行。

这个问题 mux 社区有相关的 pull request 已经解决了,截止发文之前一直都没有合并到主干。

易用性胜于性能的日志组件

日志考察了好几个,基本上都是格式化的日志,有的追求了性能,易用性差点,有的追求好用,性能又差点,考虑的发布系统的性能不是最需要考量的,最后采用了 logrus

简单的 ORM 框架

ORM 工具用的是 gorm,在我的这个场景之下,完全够用,可维护性也很好,一点教训是复杂语句尽量使用纯 SQL。

TCP Client

用了 buffstreams 这个库做发布日志回传工具,是个一个简单的 TCP client 和 TCP Server ,采用 protobuf 数据传输协议

缓存

用了 go-cache 这个工具缓存每个部署任务回传的消息,一定时间会自动清除,类似 redis 的 expire。

支持异步的系统命令执行库

使用 cmd 执行部署命令和采集部署日志,这个库封装了系统的 exec.Command 支持非阻塞的执行系统命令,支持异步获取命令的执行错误和输出

统一打包,发挥 Go 部署简单的优势

静态文件打包工具使用 packr 把前端用到的静态文件打包到 Go 的二进制程序中执行,这样发布 Go 程序只需要一个静态包即可,非常方便。

其他第三方库

发布过程中需要和 gitlab 以及文件上传服务打交道,用了一个社区的 gitlab API 库,由于使用的 consul 做服务发现与注册用,也用了 consul 官方提供的 API,文件上传用的又拍云 SDK。

消息协议

用的是 protobuf

项目结构最佳实践

由于 Go 的 import 规则对目录结构是有严格要求的,这就需要提前安排好每个 package 的功能,为的是防止一旦发生目录变动,更改 import 语句代价不小。

由于是 web 项目,整个项目结构和普通 web 项目结构很相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── Makefile
├── README.md
├── apps 后端 APP
├── db 数据库连接
├── ember-app-espire 前端 APP
├── helpers 和 model 层交互的 business layer
├── logshow 日志回显组件
├── main.go 入口文件
├── models ORM 中的 model
├── pbmessage protobuf
├── resources 表单对应的 Go struct,称之为 form struct
├── script
├── services 接口响应对应的 playload,称之为 JSON struct
├── static
├── templates html 目录
├── tests
├── utils 第三方工具
└── worker 执行部署任务的 worker

有几个原则是整个项目结构划分一直坚持的:

  • 前端 APP 提交的 form 和 后端响应返回的 playload 必须显示定义
  • 后端 APP 层要严格按照路由进行划分
  • worker 单独成包,不能依赖除 utils、service、resource 包之外的其他任何业务层的包,方便移植和拆分
  • 工具包必须要有 test case

后端 APP 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apps
├── app
│   └── app.go
├── cloud
│   └── cloud.go
├── computer
│   └── computer.go
├── consul
│   └── consul.go
├── domain
│   └── domain.go
├── gitlab
│   └── gitlab.go
├── home
│   └── home.go
├── router.go
└── service
└── service.go

后端 APP 按照业务和路由规则进行分组,不同的业务分属不同的组,在根目录的 router 中统一组装然后注册到 server 中。

这样做的好处是业务的可移植性强,如果愿意甚至都可以做热插拔。

执行任务部署的 worker

1
2
3
4
5
6
worker
├── cmd.go
├── job.go
├── log.go
├── main.go
└── run.go

worker 是单独部署的一个进程,主要是轮询部署任务,然后执行部署,实时报告部署状态和日志,分层结构是按照部署任务的步骤划分的。

独立第三方 sdk 和工具库

整个项目中都要用到日志、http client、第三方 sdk 等,这部分功能单独出来形成了项目的整个工具库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
utils
├── consul
│   └── consul.go
├── format
│   └── format.go
├── gitlab
│   └── gitlab.go
├── http
│   └── http.go
├── log
│   └── log.go
├── ucloud
│   ├── ucloud.go
│   └── ucloud_test.go
├── upyun
│   ├── upyun.go
│   └── upyun_test.go
└── util.go

第三方单独在封装就是怕第三方接口变动时需要改业务逻辑,封装之后即使第三方接口字段等发生变化,只要按照需要的格式进行一层转换即可。

选好依赖管理

项目一开始没有用任何依赖管理,后来随着依赖的不断变多,用了 dep 来做包管理,再后来发现 dep 升级包非常痛苦,这个时候 Go module 出了一段时间了,于是换上了 Go module。

Go module 比 dep 好用,速度快,而且在规则上要比 dep 简单,感兴趣的可以看 https://colobu.com/2018/08/27/learn-go-module/ 的文章对 Go module 的介绍,Go module 兼容 dep ,初始化时会自动导入 dep 管理的所有依赖。

如果你的项目可以升级到支持 Go module 的版本,或者换管理包的代价还能承受,最好换上 Go module 能省去很多不必要的麻烦。

善用 Go 的异步特性

Go 的异步支持是从语言层面就开始了,所以非常适合在同一个进程内开多个轮询任务,做工作流比使用其他语言要容易很多,因为工作流就是各种不能中断的 runner 一直执行的过程,而且各个 runner 之间需要进行消息同步。

Runner 自然用协程实现,消息同步用 chan,在实现工作流之前,务必要先设计个好需要多少个流程,每个流程之间需要同步哪些消息,提前设计好,再用代码去实现。

使用 make 自动化 build

Go 作为静态语言需要频繁的构建和发布,可以使用第三方工具自动检测 Go 源文件的变化自动 make 或者自己自己写一个定时 build 。

尽量使用环境变量来从入口 main 传入需要定制的参数,减少配置在仓库中暴露的风险,也符合 12 factor 对应用部署的要求。

三月沙 wechat
扫描关注 wecatch 的公众号