如何從 Go Vendor 模式遷移到 Modules

【導讀】本文中,作者記錄了 go 項目從 vendor 模式遷移到 go modules 的實操經驗,一起來學習吧!

本篇記錄了我們團隊是怎麼從「特殊」的 Go Vendor 模式遷移到 Go Modules,並且怎麼在每個項目中管理這些依賴

Vendoring

在介紹 Go 的 vendoring 機制之前,我們可以順便考下古,後退到 Go 還沒有包管理概念的蠻荒時代。經歷過那個時代的 Gopher 只有自己知道怎麼編譯自己寫的代碼,沒有版本管理也不保證兼容,如果你想編譯,那就要把依賴一個一個 go get 到本地。當然這樣開發起來費時費力,實在配不上 Go 簡潔高效,大道至簡的作風,於是 Go Team 接受了社區的建議,引入了 Vendor 的概念,允許代碼倉庫把自己的依賴通過 vendor directory 放在當前倉庫內,但是在官方的說明中我們可以看到一直是標註爲「Experiment」的,並且也高亮了主要的機制:

If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import “p” is interpreted as import “d/vendor/p” if that path names a directory containing at least one file with a name ending in “.go”. When there are multiple possible resolutions, the most specific (longest) path wins. The short form must always be used: no import path can contain “/vendor/” explicitly. Import comments are ignored in vendored packages.

根據當年的 proposal,之所以引入 vendor mode ,並且命名爲「vendor」,有以下幾個原因(摘自原 Google Doc):

並且根據當時的設計,Go tool 會對 vendor 目錄會進行層級搜索:也就是 當前目錄 → GOPATH → GOROOT (參考 golang/go/build 的 searchVendor 方法)

當時的社區大多數的項目採用第一種方式,也就是在當前目錄掛上 vendor 目錄,這對像 GitHub 這種開源社區來說是足夠的了,大多數項目都不是 org 級別的,只要管理自己項目的依賴就可以了。但是我們一旦我們把這種設計引用到公司項目羣級別可能就會比較頭疼了,每個項目都有自己的 vendor 依賴,這些依賴一旦設計到一些基礎組件的 sdk package,這對維護基礎組件的同學來說簡直就是噩夢,我們根本不知道那個業務線用的是哪個 out of date 的 sdk 。爲了統一第三方包,我們引入了 GOPATH 級別的 Vendor,自己用 Shell 寫了些小工具通過 git subtree 的形式維護了一個 vendor 的倉庫,跟 vendor 同 GOPATH 的項目的依賴都會收攏到這個 vendor 倉庫。這個改造在當時來說是非常值得的,也成功幫助我們在 全部服務升級 grpc 版本 ,升級 Consul 等技術改造方面避了很多坑。(衆所周知,grpc-go 版本升級的 Breaking Changes 是可以讓人大喊 真~不~戳 的

但是自從 Go 1.10 時候 rsc 的 Go & Versioning(vgo) 七連 的出現,以及 rsc 跟 sdboy 在 Twitter 上長達數十個 Thread 的激烈討論(sdboy 是 Vendoring 的支持派,並著有 golang/dep,一度引領 Go 社區的版本管理風潮 ),我就知道風向變了,我們得想個辦法把現在的體系遷移到 Go Module 。

Moduling

翻遍 Migration Guide ,最後只在角落裏面找到了,Go tool 的go mod vendor不支持除了當前目錄 vendor 之外的模式(翻了源碼,發現也確實不支持)。有點不理解爲什麼既然 vendor 支持三種路徑的 vendoring,但是go mod vendor竟然只支持一種,也許是 Go Team 加入go mod vendor本來就是像社區的一種妥協吧,或者是像羣友說的 Google 沒有這種需求 hhh。

在 Gopher Slack 上問了圈,沒人理我之後,我就打算自己搞這種場景(GOPATH Vendor)的支持了。(順便還能體驗下對 Go Module 本身的開發,等我寫完了就去 golang/go 開 issue

ezmod #1

第一個版本的實現我在團隊內部的 wiki 上寫了 design guide,得到了團隊內部的支持之後就開始開工了。

這份 design guide 的結構抽象一下會分成三個部分:

依靠着 Go Team 的 x/mod ,編碼並不是很難。我們最後得出的 go.mod 結構大概是這樣:

圖中的 ezmod.yaml 就是 Part 2 定義的 replace rule,我對於 Part 3 中的 mapping 的具體做法是,先進行 go get 生成完整的 go.mod,有 Part 2 的幫助,應該可以成功生成 go.mod。再從 index 中根據 import path 找出對應的 version,這邊有個 trick ,我們從 vendor index 拿到的 SHA-1 是不能直接用來 replace 的,需要通過 go get import_path@version 的方式拿到完整的 version 。(這是我從 k8s pin module 的腳本 中借鑑(抄)來的 ,後來意識到好像可以用 x/mod/semver 直接糊,還少了很多次 Network I/O )

OK,花一天寫完了這個 Generator (大部分事件在 debug),拿我們基礎組件倉庫做了個實驗,確實 replace 規則都按照預期生成了,看起來也很完美,commit module, 打好 tag,cc 組內同事,下班搓爐石!

ezmod #2

第二天組內小夥伴用這工具給業務項目打 mod 的時候,遇到了一個問題:

業務項目依賴基礎庫的時候,好多三方庫的 version 都不對,因爲 API Changes 的關係,編譯都無法成功。

查了問題之後發現,基礎庫的 module replace 規則都沒有生效。我們做了一個最小的重現 case,果然,依賴的 go.mod 裏面的 replace 規則會被忽略。也就是說:

A -> B -> C1 // A 依賴 B,B 依賴 C, 但是 B 的 go.mod 把 C 替換成了 C1
A -> B -> C // A 中不會使用 B 中 go.mod 中的 replace rule,B 依舊依賴 C
A ->replace C => C1 -> B -> C1 // 只有 A 中的 replace rule 中也增加 C=>C1,A 中才會使用 C1

這個問題解決方案其實也不難得出:我們的 mapping 要改,在全部項目裏面根據 vendor index 全量生成 replace 規則就可以了。但是對於我們幾百個子倉庫的 vendor 來說,全量生成非常耗時。於是,我們開始思考新的解決方案。

「如果我們已經知道了依賴的版本,我們直接 require 不就好了,爲什麼一定要 replace」,同事的這句話倒是給我提供了新的思路。於是,開始改寫部分代碼把 vendor index 裏的依賴全部 require 到了 module 裏面。但是原 本的 replace 版本真的一無是處了嗎 ?我倒是覺得未必。

在 go mod 的開發中,replace 經常被用來作爲本地 module 的替換,可以用來作爲 debug 或者用來做本地測試。結合這點,我們如果可以把之前的模式跟這個場景結合起來,提供一個本地 develop 模式。對應的具體場景可能是:1) 我們可以方便得找到項目中的依賴,並把它 replace 成某個還沒有正式發佈 release tag 的依賴。2) 只要我們 require 也是根據 vendor index 生成的,那麼這個 replace 只在當前倉庫中生效,這樣即使在聯調環境我們也還是可以使用 develop 模式下的 module,來應付一些基礎庫在聯調環境下的差異化改造。3 ) 真正發佈(生產 / 測試)時,需要 CI 移除掉之前的 module,並生成一份新的 publish module,這個 module 裏面會把 vendor index replace 規則刪掉,使用 stable 的依賴。

通過這種方式,我們把開發分成了兩種角色:

這樣就還是可以保證我們的所有生產 / 測試環境 (publish) 的依賴還是可以在 vendor 裏面收攏的,不會又變得不可控。

總結

其實看得出來,我們這次改造是爲了遷移 mod 而遷移到 Go Module,真正的依賴管理還是會依靠 vendor 和 vendor index,我們更新某個三方依賴時,還是需要去把它 merge 到 vendor 裏面,再通過 vendor index 提供給外部項目的 Go Module 中使用。相比之前還是有些優勢的,之前有新人入職配置環境或者我們配置 CI pipeline 時,由於 vendor 這個東西實在有點大,流程會卡在下載 vendor 上面非常久的時間,現在只要通過 ezmod 去拉取一份 vendor index 文件就好了,頓時從幾十 GiB 下降到了幾十 KiB。同時也能繼續保證所有依賴能在一個地方 (vendor) 統一管控。

這樣方案也可以看作是對 go mod vendor 的一個擴展補充吧。

以上。

轉自:

blog.scnace.me/post/go-module-migration

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/izSWCYfPDoQbEbPjHvcQPg