即將發布的 Go 語言 1.11 版本將會給我們帶來對 *modules*(模組)的實驗性支援,這是 Go 語言新的一套依賴管理系統。(譯註:很多程式設計語言中,把 modules 譯作「模組」,但由於目前該機制在 Go 語言還沒正式發布,因此尚未有非常普及的譯法。而類似的 vendor 一詞的翻譯,大多中文文章都是採取保留英文原文的方式處理,因此本文對 modules 的翻譯參考 vendor 的處理:保留英文原文)前些日子,[我簡單地寫了一編關於它的文章](https://roberto.selbach.ca/playing-with-go-modules/),自從那篇文章寫完後,Go 的 modules 機制又發生了一些小改動。因為現在快到正式版發布了,我想現在剛好是時候用”邊做邊學“的風格來寫一篇關於它的文章。所以接下來我們要做的是:我們將會建立一個新的包,並且我們會發布幾個版本,看看它們是怎麼工作的。## 建立一個 Module第一件事情,我們先建立一個叫 `testmod` 的包。注意這裡有個重要的細節:**它的目錄要在 `$GOPATH` 之外,因為預設情況下,`$GOPATH` 裡面是禁用 modules 支援的。**Go 的 modules 機制在某種程度上,是消滅整個 `$GOPATH` 的第一步。```bash$ mkdir testmod$ cd testmod``` 我們的包非常的簡單:```gopackage testmodimport "fmt"// Hi 返回一個友好的問候func Hi(name string) string { return fmt.Sprintf("Hi, %s", name)}```這個包已經寫完了,但是現在還不是一個 module,我們來把它初始化為 module:```bash$ go mod init github.com/robteix/testmodgo: creating new go.mod: module github.com/robteix/testmod```上面的命令在目前的目錄裡面建立了一個名為 `go.mod` 的檔案,它的內容如下:```module github.com/robteix/testmod```並沒有很多東西,但是它已經有效地使我們的包變成一個 module 了。我們現在可以把這個程式碼推送到代碼倉庫裡面了:```bash$ git init$ git add *$ git commit -am "First commit"$ git push -u origin master```(譯註:在 `git push` 之前,你可能還要添加遠程倉庫地址,例如:`git remote add origin https://github.com/robteix/testmod.git`)到目前為止,任何想要用這個包的人,都可以 `go get` 之:```bash$ go get github.com/robteix/testmod```上述的命令會擷取 `master` 分支上最新的代碼。這個方法依然湊效,但是我們現在最好不要再這麼做了,因為我們有更棒的方法了。擷取 `master` 分支有潛在的危險,因為我們不能確定,包作者對包的改動會不會破壞掉我們的項目對該包的使用方式。(譯註:也就是說,我們不能確定當前 `master` 分支的代碼是否保持了對舊版本代碼的相容性)。而這個就是 modules 機制旨在解決的問題。## 簡單介紹一下模組版本化Go 的 modules 是*版本化的*,並且某些版本有特殊的含義,你需要瞭解一下 [語意版本控制](https://semver.org/) 背後的概念。更重要的是,Go 是根據代碼倉庫的標籤來確定版本的,並且有些版本跟其它版本是不一樣的:比如版本 2 以上應該跟版本 0 和版本 1 有不同的匯入路徑(我後面會講到這個)。還有,預設情況下 Go 會擷取代碼倉庫裡面設定好標籤的最新的版本,只是一個很重要的知識點,因為你可能已經習慣了在 `master` 分支工作。到目前為止,你現在要記得的是,要製作程式碼封裝的一個發布版本,我們需要給我們的代碼倉庫打上版本標籤。所以,現在讓我們開始吧。## 製作我們的第一個發布版本我們的包已經準備好了,現在我們可以向全世界發布它。我們通過版本標籤來實現這個發布,現在,我們一起來發布我們的 1.0.0 版本:```bash$ git tag v1.0.0$ git push --tags```上述命令在我們的倉庫上面建立了一個標籤,標記了我們當前的提交為 1.0.0 版本。雖然 Go 沒有強制要求,但是我們最好建立還是一個叫 `v1` 的分支,這樣我們可以把針對這個版本的 bug 修複推送到這個分支:```bash$ git checkout -b v1$ git push -u origin v1```現在我們可以切換到 `master` 分支,做自己要做的事情,而不用擔心會影響到我們已經發布的 1.0.0 版本的代碼。## 使用我們的 module現在我們準備好可以使用我們的 module 了,下面我們建立一個簡單的程式來使用我們剛才做的包:```gopackage mainimport ( "fmt" "github.com/robteix/testmod")func main() { fmt.Println(testmod.Hi("roberto"))}```到現在,你可以 `go get github.com/robteix/testmod` 來下載這個包。但是對於 module 來說,事情就變得有趣了。首先我們需要在我們新的程式裡面啟用 module 功能:```bash$ go mod init mod```正如之前所發生的那樣,上面的命令會建立一個 `go.mod` 檔案,它的內容是:```module mod```當我們嘗試構建我們的程式時,事情變得更加有趣了:```bash$ go buildgo: finding github.com/robteix/testmod v1.0.0go: downloading github.com/robteix/testmod v1.0.0```正如我們看到的,`go` 命令自動地擷取程式匯入的包。如果我們看看程式的 `go.md` , 我們可以看到內容出現了變化:```module modrequire github.com/robteix/testmod v1.0.0```而且我們還多了一個叫 `go.sum` 的檔案,它包含了各個包的雜湊值,用以保證我們擷取到了正確的版本和檔案:```github.com/robteix/testmod v1.0.0 h1:9EdH0EArQ/rkpss9Tj8gUnwx3w5p0jkzJrd5tRAhxnA=github.com/robteix/testmod v1.0.0/go.mod h1:UVhi5McON9ZLc5kl5iN2bTXlL6ylcxE9VInV71RrlO8=```## 為一個發布了的版本修複漏洞假如說,我們現在發現我們的包出現了一個漏洞:歡迎語漏了一個標點符號!人們開始生氣了,因為我們友好的歡迎語並不夠友好。所以我們趕緊修複這個漏洞並發布一個新版本:```go// Hi 返回一個友好的歡迎語func Hi(name string) string {- return fmt.Sprintf("Hi, %s", name)+ return fmt.Sprintf("Hi, %s!", name)}```我們在 `v1` 分支做這些改動,因為這個 bug 只在 `v1` 版本中存在。當然,在實際的情況中,我們很有可能需要把這個改動應用到多個版本,這時候你可能就需要在 `master` 分支做這些改動,然後再向後移植(譯註:back-port 或稱 backporting, 參考[維基百科](https://zh.wikipedia.org/wiki/%E5%90%91%E5%BE%8C%E7%A7%BB%E6%A4%8D) )。無論怎樣,我們都需要在 `v1` 分支上有這些改動,並且把它標記為一個新的發布:```bash$ git commit -m "Emphasize our friendliness" testmod.go$ git tag v1.0.1$ git push --tags origin v1```## 更新 modules預設情況下,Go 不會自己更新模組,這是一個好事因為我們希望我們的構建是有可預見性(predictability)的。如果每次依賴的包一有更新發布,Go 的 module 就自動更新,那麼我們寧願回到 Go v1.11 之前沒有 Go module 的荒莽時代了。所以,我們需要更新 module 的話,我們要顯式地告訴 Go。我們可以使用我們的老朋友 `go get` 來更新 module:- 運行 `go get -u` 將會升級到最新的*次要版本*或者*修訂版本*(比如說,將會從 1.0.0 版本,升級到——舉個例子——1.0.1 版本,或者 1.1.0 版本,如果 1.1.0 版本存在的話)- 運行 `go get -u=patch` 將會升級到最新的修訂版本(比如說,將會升級到 1.0.1 版本,但**不會**升級到 1.1.0 版本)- 運行 `go get package@version` 將會升級到指定的版本號碼(比如說,`github.com/robteix/testmod@v1.0.1`)(譯註:語義化版本號碼規範把版本號碼如 v1.2.3 中的 1 定義為主要版本號碼,2 為次要版本號碼,3 為修訂版本號碼 )上述列舉的情況,似乎沒有提到如何更新到最新的主要版本的方法。這麼做是有原因的,我們之後會說到。因為我們的程式使用的是包 1.0.0 的版本,並且我們剛剛建立了 1.0.1 版本,下面任意一條命令都可以讓我們程式使用的封裝更新到 1.0.1 版本:```bash$ go get -u$ go get -u=patch$ go get github.com/robteix/testmod@v1.0.1```運行完其中一個(比如說 `go get -u`)之後,我們的 `go.mod` 檔案變成了:```module modrequire github.com/robteix/testmod v1.0.1```## 主要版本號碼根據語義化版本的語義,主要版本與次要版本是不同的,主要版本可以打破向後相容性。從 Go modules 的角度來說,一個包,如果兩個主要版本號碼不同的話,那這它們相當於兩個完全不同的包。這聽起來很玄乎,但是它是合理的:如果一個包的兩個版本不能相容的話,它就是兩個不同的包。我們來為我們的包做一個主要版本號碼的改變,怎麼樣?我們發現我們的 API 太過簡單,對於我們使用者的用例限制太多,所以我們給 `Hi()` 函數加多一個參數,來指定歡迎語的語言:```gopackage testmodimport ( "errors" "fmt")// Hi 返回一個歡迎語,其語言由 lang 指定func Hi(name, lang string) (string, error) { switch lang { case "en": return fmt.Sprintf("Hi, %s!", name), nil case "pt": return fmt.Sprintf("Oi, %s!", name), nil case "es": return fmt.Sprintf("¡Hola, %s!", name), nil case "fr": return fmt.Sprintf("Bonjour, %s!", name), nil case "cn": return fmt.Sprintf("你好,%s!", name), nil default: return "", errors.New("unknown language") }}```以前使用我們的包的項目,如果直接使用現在這個新的版本,它們將不能編譯通過,因為它們沒有傳遞 `lang` 參數,並且它們沒有接收返回的 `error` 錯誤。所以我們的 API 與 v1.x 版本的 API 不能相容,是時候躍進新的2.0.0時代啦!我之前提到的,某些版本有特殊的含義,現在就是這種情況,版本 2 和更高版本需要改變匯入路徑,它們已經是不同的包了。我們需要在我們 module 名字後面添加一個新的版本路徑:```module github.com/robteix/testmod/v2```剩下的事情跟我們之前做的一樣,我們把它標記成 v2.0.0,並推送到遠程倉庫(並且可選地,我們還能添加一個 `v2` 分支)```bash$ git commit testmod.go -m "Change Hi to allow multilang"$ git checkout -b v2 # 可選的,但是推薦這麼做$ echo "module github.com/robteix/testmod/v2" > go.mod$ git commit go.mod -m "Bump version to v2"$ git tag v2.0.0$ git push --tags origin v2 # 如果沒有建立 v2 分支,就推送到 master 分支```## 更新到一個新的主要版本雖然剛剛我們的包發布了一個新的版本,而且這個新的版本並不能向後相容,但是使用我們的包的現有項目卻不會受影響。因為它們還是會繼續使用現有的 1.0.1 版本,`go get -u` 不會把項目使用的包的版本升級到 2.0.0某些情況下,我作為一個庫的使用者,可能會希望升級到 2.0.0 版本,因為我可能需要用到多語言的支援。要這麼做的話,我要相應的修改我的程式:```gopackage mainimport ( "fmt" "github.com/robteix/testmod/v2" //注意包匯入路徑變了)func main() { g, err := testmod.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g)}```然後我再執行一下 `go build` ,它會自動幫我擷取 v2.0.0 版本的包。要注意的是,雖然現在包匯入路徑是以 "v2" 結尾的,但是我們在代碼裡面依然用 `testmod` 這個包名來引用它。正如我之前所提到的,兩個主要版本號碼不同的包,在各種目的和意圖上,都是兩個不同的包。Go modules 並不會把這兩個包連結在一起,這意味著我們可以在程式裡面同時使用這個包的兩個不同的主要版本:```gopackage mainimport ( "fmt" "github.com/robteix/testmod" testmodML "github.com/robteix/testmod/v2")func main() { fmt.Println(testmod.Hi("Roberto")) g, err := testmodML.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g)}```這解決了依賴管理方面的一個常見的問題:當項目依賴於同一個庫的兩個不同版本時,該如何處理。## 整理一下回到我們之前那個只用了 testmod 2.0.0 的程式,如果我們看一下 `go.mod` 的內容,我們會發現:```module modrequire github.com/robteix/testmod v1.0.1require github.com/robteix/testmod/v2 v2.0.0```預設情況下,Go 並不會在 `go.mod` 上面移除掉依賴項,除非你明確地指示它這麼做。如果你希望能夠清理掉那些不再需要的依賴項,你可以使用新的 `tidy` 命令:```bash$ go mod tidy```現在剩下的依賴項都是我們項目中使用到的了。## Vendor 機制預設情況下,Go modules 會忽略 `vendor/` 目錄。這個想法是最終廢除掉 vendor 機制[^1]。但如果我們仍然想要在我們的版本管理中添加 vendor 機制管理依賴,我們還是可以這麼做的:```bash$ go mod vendor```這會在你項目的根目錄建立一個 `vendor/`目錄,並包含你的項目的所有依賴項。即使如此,`go build` 預設還是會忽略這個目錄的內容,如果你想要構建的時候從 `vendor/` 目錄中擷取依賴的代碼來構建,那麼你需要明確的指示:```bash$ go build -mod vendor```我猜想大多數要使用 vendor 機制的開發人員,在他們自己的開發機器上會使用 `go build` ,而在他們的CI系統(Continuous Integration,持續整合)上則使用 `-mod vendor` 選項還有,對於那些不想要直接依賴版本控制服務(譯註:比如 github.com)上遊代碼的人來說,比起用 vendor 這種機制,更好的方法是使用 Go module 代理。有很多方法可以保證 `go` 不會連網去擷取包代碼(比如 `GOPROXY=off`),但這些內容只能在之後的文章提及了。## 結論這篇文章看起來比較嚇人,但我盡量的把很多事情放在一塊解釋了。事實是,現在 Go 的 modules 機制基本上是透明的,我們像往常一樣在我們的代碼裡面匯入包,`go` 會處理剩下的事情。當我們構建程式的時候,它的依賴項會被自動地擷取。Go 的 module 還消除了 `$GOPATH` 的使用, `$GOPATH` 曾經使得很多 Go 開發新手難以理解為什麼所以東西都要放到一個特定的目錄。~~Vendor 機制已經被使用 module 代理的方法取代了~~[^1],我大概會專門新開一篇關於 Go module 代理的的文章。---[^1]: 我覺得好像這麼說似乎語氣太重了,讓人覺得好像 vendor 機制就要立刻被廢棄了似的,並不是這樣的,vendor 機制還能用,儘管和以前略有不同。似乎總有想要用更好的東西替代 vendor 機制的願望,或許這個替代方案就是 module 代理,又或許不是。但現在就是這樣的情況:人們有想要用更好的方案替代 vendor 機制的願望,但 vendor 機制在一個更好替代方案(如果真的能有更好的替代方案)出現之前,是不會廢棄的。
via: https://roberto.selbach.ca/intro-to-go-modules
作者:Roberto Selbach 譯者:Alex-liutao 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
707 次點擊 ∙ 2 贊