曹大吐槽 go mod

从 rsc 据理力争设计并将 go mod 集成在 Go 语言中,已经两年过去了,时至今日,广大 Gopher 仍是常常被 go mod 相关的问题折磨。git

本文会列举一些我和个人同事使用 go mod 时碰到的问题,有些问题是 go mod 自己的问题,有些多是第三方 goproxy 实现的问题。github

若是你作过比较大型的 go 项目开发,相信总会有那么几个让你会心一笑。golang

Go 命令的反作用

从老版本一路升级过来的 gopher 很难理解为何升级了新版本以后,go fmt 一个文件都变得很是卡顿。算法

go 的不少子命令都在引入 go mod 后增长了反作用,如 go test,go fmt(ide 经常使用),go build,go list(ide 经常使用)。npm

例如上面的 go fmt,我只是想格式化一下个人文件,并无想下载依赖,但仍是得耐心等依赖下载完毕。缓存

go test 时会自动修改 go.mod 文件就更使人困惑了:why go mod keeps changing with go testgo.mod be modified after go testmarkdown

这也是 go.mod 和 go.sum 为何老是会出如今咱们的文件变动列表里。况且这两个文件在大项目开发的时候又尤为容易冲突。less

go.sum git 合并冲突

当不少同事在同一个 git 仓库中作开发时,即便咱们已经划分好了工做职责,在代码合并的时候仍是没有办法 auto merge:ide

相似上面这样的合并冲突,下面躺着 go.sum 的状况相信你也见过不少了。oop

形同虚设的 semver 规范

go mod 的设计认为社区是严格遵照 semver 的规范的:

Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards compatible manner, and
PATCH version when you make backwards compatible bug fixes.

小版本升级,如 1.7.4 -> 1.7.5 不该该引入不兼容升级,不过显然 Google 高估了开源社区的节操。很多开源库做者 API 修改起来都比较随便。

即便是 Google 本身的 grpc-go 项目,也在小版本升级中干过不兼容的事情:Update your SemVer Policy Please - Breaking changes in minor versions causing heartache

况且 grpc-go 的做者还光明正大地认可,他们在 semver 的前提下,依然容许一些不兼容的 例外

甚至还有那些从 release notes 中不易察觉的 behavior change 致使依赖 grpc-go 的 helm 项目在生产环境中遇到了 bug,使人大为光火。

好样的,Google 工程师。

除了人的问题以外,在 semver 规范中还存在一种例外状况:

Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.

go mod 设计时并未考虑这种状况,mvs 算法在 0.y.z 范围内也会尽可能在大版本不变的状况下,无情地帮你升级小版本,搞的百姓怨声载道,苦不堪言。

这两年爆火的云原生领域,有不少项目在 0.x.y 版本一待就是两三年。从业者依赖 0.x 的版本号再正常不过了。若是你问 go mod replace 谁用的最溜?那想必是云原生开发者啦。

版本信息扩散

因为 go mod 的设计,若是一个依赖库升级了新版本,咱们的 import 路径就会发生变化:

chi 项目升级 v5 了,全部引入 chi 下 lib 的代码都须要改 import,开心不开心。咱们又要升级兼容新的 API,又要改这些处处散落的 import path。

这绝对不能说是优秀的设计。

goproxy 的实现各不相同

由于特殊缘由,国内的 gopher 基本都须要配置国内公司 / 我的开发的 goproxy 来加速依赖下载,这些 proxy 没有使用相同的代码,因此实现细节上常常会有差异。

例如,当某个库不存在时,有的 goproxy 返回 404,而有的 goproxy 返回 500(这是笔者使用某司 goproxy 时的真实状况),匪夷所思。

咱们来看一下更加使人诧异的例子,来帮你理解这种匪夷所思。

删库跑路

简单作个实验,听从如下步骤:

  1. 在 github 上建立仓库 A
  2. 经过 goproxy X 来 go build
  3. 删除仓库 A
  4. 删除 mod cache,并使用 goproxy X/Y/Z 分别执行 go build
第一次 go build 删库后 goproxy.cn 删库后 goproxy.io 删库后 腾讯 goproxy 删库后 aliyun goproxy
goproxy.cn 可 build 不可 build 不可 build 不可 build
goproxy.io 可 build 可 build 不可 build 不可 build
腾讯 goproxy 可 build 不可 build 可 build 不可 build
aliyun goproxy 可 build 不可 build 不可 build 可 build

此次选取了国内使用最普遍的四个 goproxy,使用其中之一缓存过一次的外部依赖,在删库后仍是能够 build 的。但若是以前未经该 goproxy 缓存的依赖,目前只有 goproxy.cn 依然可以正常地下载依赖。

通过对原做者的咨询,目前 goproxy.cn 在未找到依赖,但 gosumdb 中有值时,会去官方的 index.golang.org 上进行查找,而 gosumdb 中有值时,通常状况下官方的 proxy.golang.org 中会有相应的缓存 (即便你设置的是第三方 goproxy)。这时 goproxy.cn 也会将从官方 goproxy 中拉取,因此用户的 build 仍是能成功的。

一个不带 vendor 的项目,理论上就会出现由于 gopher 使用的 GOPROXY 不同,致使薛定谔的 build 结果。

若是咱们细看一下 sum.golang.org,官方对外部库的缓存期限描述也是比较模糊的。

模糊的存储期限

proxy.golang.org does not save all modules forever. There are a number of reasons for this, but one reason is if proxy.golang.org is not able to detect a suitable license. In this case, only a temporarily cached copy of the module will be made available, and may become unavailable if it is removed from the original source and becomes outdated. The checksums will still remain in the checksum database regardless of whether or not they have become unavailable in the mirror.

上面这段话来自 sum.golang.org,从官方的这种说法来看,依赖库在 goproxy 中的存储并非永久的,至少在 proxy.golang.org 中不是永久的,官方给出的 a number of reasons 也很是的模糊。

咱们没有办法把工做赌在这种虚无缥缈的措辞上,只能认为 goproxy 不会永久缓存咱们的仓库。没有办法期望咱们的依赖可以永远存在。原仓库从 github 消亡以后,早晚有一天也会在各个 goproxy 上消亡,reproducible build 沦为笑谈。

即便在 go mod 推出的两年后,对于咱们来讲,把依赖保存在 vendor 中依然是必要的。

多年前,left pad 在 js 社区引发的悲剧,也许并无给当前的软件设计者提供多少教训:
how one programmer broke the internethave we forgotten how to program

相关文章
相关标签/搜索