早在 1.18 版本,Go 就引入了 workspace 功能来改善多 module 开发的体验。网上关于 workspace 功能的介绍大多局限于玩具项目内的开发,并无多少实际的案例。正巧 mosn/htnn 这个项目就深度依赖 workspace 功能,而且它也足够复杂,可以拿来说明真实世界里面的 go work 体验。
为什么选择 go work?
在最早的版本里,htnn 整体就是一个 module。然而当第三方开发者想要基于 htnn 开发插件时,发现这样做不得不依赖整个 htnn 和 htnn 它自己的依赖。所以为了避免不必要的依赖地狱,我们决定把 htnn 拆成不同的 go module。由于我们希望规避复杂的多仓库 PR 之舞,以及不同 go module 间频繁的版本变化,所以用 go work 来实现单一仓库多 module 的功能。这样一来,在其中一个 go module A 里新加了方法,无需改动另外一个 go module B 的 go.mod文件,就能直接用同一个 PR 在 B 里采纳该方法。这对于一个处于早期、迭代频繁的项目来说是必不可少的。
为什么提交 go.work 文件?
GitHub 默认的 gitignore 会忽略 go.work 文件。这里有说明 为什么推荐忽略go.work。但我们选择把它提交到 git 仓库里。因为如果没有提交 go.work
到仓库中,开发者 clone 了 htnn 之后需要自己去配置 go.work
,才能把测试跑起来。我们希望尽可能降低开发者上手的成本,让每个开发者的开发体验达到“开箱即用”的水平,而不是要求每个开发者都去接受 go.work
教育。当然我们还是把 go.work.sum
加入到 gitignore,因为 go 命令行会自动生成这个文件,不需要 htnn 提前准备。
为了避免因为 go.work
的存在,导致某些组件在跑 CI 时选择错误的版本,我们特意在跑某些 CI job 时把 go.work
删掉。比如在测试示例代码时,专门在开头加入一行 rm go.work
:https://github.com/mosn/htnn/blob/78c9bf58432b0485f4a3f11094e6b9c411fd4ff4/.github/workflows/test.yml#L237
go work 用起来顺滑吗?
大体上是的。但还是有一些问题。
在 go work 下无法正常使用非 workspace 的 go module
我们在 tools 下面用 Go 写了一些工具。如果没有把 tools 加入到 go.work
里,在运行时会报错:
$ go run cmd/linter/main.go
cmd/linter/main.go:29:2: no required module provides package github.com/yoheimuta/go-protoparser/v4; to add it:
go get github.com/yoheimuta/go-protoparser/v4
cmd/linter/main.go:30:2: no required module provides package github.com/yoheimuta/go-protoparser/v4/parser; to add it:
go get github.com/yoheimuta/go-protoparser/v4/parser
结果我们只好把 tools 也放到 go.work
里。代价是打包镜像时需要跑一个 sed 脚本把它从 go.work
里去掉,毕竟这些工具和生产环境上运行的代码无关。
不仅仅是运行 main.go 时会报错,像是 gopls 这样的工具也无法在非 workspace 的 module 下运行。举个例子,我在 htnn 项目下面 clone 了 istio 来做二次开发,如果不修改 go.work
把 istio 的目录加上,那么就没办法在 istio 的代码里使用代码补全。
go mod tidy 不支持 go work
go mod tidy 无法识别 go.work
文件,参见 https://github.com/golang/go/issues/50750。
一旦 go module A 依赖了 go module B 新增的 package,go mod tidy 就会失败。所以我们不得不给 go mod tidy 加上 -e 选项,让它忽略这种失败。
由于 go mod tidy 不支持 go work,而且 go mod tidy 在解决依赖时,如果 go mod 里记录的版本无法解决问题,会主动升级最新版本(即使升级到最新版本依然解决不了问题!)。后果是,每次当我们发布完 go module,在现存的其他 PR 上执行 go mod tidy 都会导致 go.mod
文件被升级到最新的 go module 上。又由于我们在 CI 里会通过 go mod tidy 检查依赖管理是否正确,为让 CI 通过,开发者不得不把这种意料之外的升级提交到自己的代码里,哪怕开发的功能实际上不要求最新的 go module。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。