Go mod 之痛
原文地址:?https://mp.weixin.qq.com/s/ihtvAyJLtwvAXVkBuqL8kw
從 rsc 力排眾議設(shè)計并將 go mod 集成在 Go 語言中,已經(jīng)兩年過去了,時至今日,廣大 Gopher 還是經(jīng)常被 go mod 相關(guān)的問題折磨。
本文會列舉一些我和我的同事使用 go mod 時碰到的問題,有些問題是 go mod 本身的問題,有些可能是第三方 goproxy 實現(xiàn)的問題。
如果你做過比較大型的 go 項目開發(fā),相信總會有那么幾個讓你會心一笑。
Go 命令的副作用
從老版本一路升級過來的 gopher 很難理解為什么升級了新版本之后,go fmt 一個文件都變得非??D。
go 的很多子命令都在引入 go mod 后增加了副作用,如 go test,go fmt(ide 常用),go build,go list(ide 常用)。

例如上面的?go fmt
,我只是想格式化一下我的文件,并沒有想下載依賴,但還是得耐心等依賴下載完畢。
go test 時會自動修改 go.mod 文件就更令人困惑了:why go mod keeps changing with go test[1],go.mod be modified after go test[2]。
這也是 go.mod 和 go.sum 為什么總是會出現(xiàn)在我們的文件變更列表里。何況這兩個文件在大項目開發(fā)的時候又尤其容易沖突。
go.sum git 合并沖突
當很多同事在同一個 git 倉庫中做開發(fā)時,即使我們已經(jīng)劃分好了工作職責(zé),在代碼合并的時候還是沒有辦法 auto merge:

類似上面這樣的合并沖突,下面躺著 go.sum 的情況相信你也見過很多了。
形同虛設(shè)的 semver 規(guī)范
go mod 的設(shè)計認為社區(qū)是嚴格遵守 semver 的規(guī)范的:
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 不應(yīng)該引入不兼容升級,不過顯然 Google 高估了開源社區(qū)的節(jié)操。不少開源庫作者 API 修改起來都比較隨便。
即使是 Google 自己的 grpc-go 項目,也在小版本升級中干過不兼容的事情:Update your SemVer Policy Please - Breaking changes in minor versions causing heartache[3]。
何況 grpc-go 的作者還光明正大地承認,他們在 semver 的前提下,依然允許一些不兼容的?例外[4]。
甚至還有那些從 release notes 中不易察覺的?behavior change[5]?導(dǎo)致依賴 grpc-go 的 helm 項目在生產(chǎn)環(huán)境中遇到了 bug,令人大為光火。
好樣的,Google 工程師。
除了人的問題之外,在 semver 規(guī)范中還存在一種例外情況:
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 設(shè)計時并未考慮這種情況,mvs 算法在 0.y.z 范圍內(nèi)也會盡量在大版本不變的情況下,無情地幫你升級小版本,搞的百姓怨聲載道,苦不堪言。
這兩年爆火的云原生領(lǐng)域,有很多項目在 0.x.y 版本一待就是兩三年。從業(yè)者依賴 0.x 的版本號再正常不過了。如果你問 go mod replace 誰用的最溜?那想必是云原生開發(fā)者啦。
版本信息擴散
由于 go mod 的設(shè)計,如果一個依賴庫升級了新版本,我們的 import 路徑就會發(fā)生變化:

chi 項目升級 v5 了,所有引入 chi 下 lib 的代碼都需要改 import,開心不開心。我們又要升級兼容新的 API,又要改這些到處散落的 import path。
這絕對不能說是優(yōu)秀的設(shè)計。
goproxy 的實現(xiàn)各不相同
因為特殊原因,國內(nèi)的 gopher 基本都需要配置國內(nèi)公司/個人開發(fā)的 goproxy 來加速依賴下載,這些 proxy 沒有使用相同的代碼,所以實現(xiàn)細節(jié)上經(jīng)常會有差別。
例如,當某個庫不存在時,有的 goproxy 返回 404,而有的 goproxy 返回 500(這是筆者使用某司 goproxy 時的真實情況),匪夷所思。
我們來看一下更加令人詫異的例子,來幫你理解這種匪夷所思。
刪庫跑路
簡單做個實驗,遵從以下步驟:
在 github 上創(chuàng)建倉庫 A
通過 goproxy X 來 go build
刪除倉庫 A
刪除 mod cache,并使用 goproxy X/Y/Z 分別執(zhí)行 go build
第一次 go build刪庫后 goproxy.cn刪庫后 goproxy.io刪庫后 騰訊 goproxy刪庫后 aliyun goproxygoproxy.cn可 build不可 build不可 build不可 buildgoproxy.io可 build可 build不可 build不可 build騰訊 goproxy可 build不可 build可 build不可 buildaliyun goproxy可 build不可 build不可 build可 build
這次選取了國內(nèi)使用最廣泛的四個 goproxy,使用其中之一緩存過一次的外部依賴,在刪庫后還是可以 build 的。但如果之前未經(jīng)該 goproxy 緩存的依賴,目前只有 goproxy.cn 依然能夠正常地下載依賴。
經(jīng)過對原作者的咨詢,目前 goproxy.cn 在未找到依賴,但 gosumdb 中有值時,會去官方的 index.golang.org 上進行查找,而 gosumdb 中有值時,一般情況下官方的 proxy.golang.org 中會有相應(yīng)的緩存(即使你設(shè)置的是第三方 goproxy)。這時 goproxy.cn 也會 從官方 goproxy 中拉取,所以用戶的 build 還是能成功的。
一個不帶 vendor 的項目,理論上就會出現(xiàn)因為 gopher 使用的 GOPROXY 不一樣,導(dǎo)致薛定諤的 build 結(jié)果。
如果我們細看一下 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 社區(qū)引起的悲劇,也許并沒有給當前的軟件設(shè)計者提供多少教訓(xùn):?how one programmer broke the internet[6],have we forgotten how to program[7]。
[1]
why go mod keeps changing with go test:?https://stackoverflow.com/questions/61625796/why-go-mod-keeps-changing-with-go-test
[2]go.mod be modified after go test:?https://github.com/golang/go/issues/30921
[3]Update your SemVer Policy Please - Breaking changes in minor versions causing heartache:?https://github.com/grpc/grpc-go/issues/3798
[4]例外:?https://github.com/grpc/grpc-go/blob/master/Documentation/versioning.md
[5]behavior change:?https://codeengineered.com/blog/2018/golang-vgo-semver-human-error/
[6]how one programmer broke the internet:?https://qz.com/646467/how-one-programmer-broke-the-internet-by-deleting-a-tiny-piece-of-code/
[7]have we forgotten how to program:?https://www.davidhaney.io/npm-left-pad-have-we-forgotten-how-to-program/
本文使用 文章同步助手 同步