2

早在 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.workhttps://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。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.