曹大吐槽 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

相關文章
相關標籤/搜索