Go的包管理工具(三):Go Modules

aoho發表於2019-03-06

在前面的文章,我們先是介紹了Go 的幾種包管理方式,然後具體介紹了一種包管理的工具: glide。隨著 Go 1.11 的釋出,官方的包管理工具 Go Modules 變得流行起來。在釋出不久的 Go 1.12 版本中,增強了對 Go Modules 的支援。本文將會介紹如何在專案中安裝和使用 Go Modules

安裝和啟用 Modules 的支援

前置條件

如本文開頭所說,從 Go 1.11 版本才支援 Go Modules。所以,預設 Go 的版本為 >= 1.11。

$ go version
go version go1.12 darwin/amd64
複製程式碼

筆者安裝了最新的 1.12 版本。

啟用使用

安裝後,我們可以通過以下兩種方式之一啟用模組支援:

  • $GOPATH/src 之外的目錄中呼叫 go 命令,且當前目錄或其任何父目錄中使用有效的 go.mod 檔案,並且環境變數 GO111MODULE 未設定(或顯式設定為auto)。
  • 在環境變數集上設定 GO111MODULE = on 後,呼叫go命令。

如何定義模組

為當前的專案建立一個 go.mod 檔案。

當專案不在 GOPATH 中,直接執行:

go mod init
複製程式碼

否則,會出現如下的錯誤:

go: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'
複製程式碼

因此,我們需要手動啟用 Modules:

$ export GO111MODULE=on 
複製程式碼

然後才能執行 go mod init。這會將任何現有的dep Gopkg.lock 檔案或其他九種支援的依賴關係轉換,新增 require 語句以匹配現有配置。

go mod init通常能夠使用輔助資料(例如VCS後設資料)來自動確定相應的模組路徑,但是如果 go mod init 表明它不能自動確定模組路徑,或者如果你需要以其他方式覆蓋 path,你可以提供模組路徑作為 go mod init 的可選引數,例如:

$ go mod init modtest
複製程式碼

構建模組

從模組的根目錄執行時,./... 模式匹配當前模組中的所有包。 go build 將根據需要自動新增缺失或未轉換的依賴項,以滿足此特定構建呼叫的匯入:

$ go build ./...
複製程式碼

測試模組

$ go test ./...
複製程式碼

按配置測試模組,以確保它適用於所選版本。還可以執行模組的測試以及所有直接和間接依賴項的測試以檢查不相容性:

$ go test all
複製程式碼

實戰

建立專案

建立專案並進入根目錄:

$ mkdir src/hello
$ cd src/hello
複製程式碼

初始化

$ go mod init github.com/keets2012/hello
go: creating new go.mod: module github.com/keets2012/hello
複製程式碼

go mod 初始化,並命名包名為 github.com/keets2012/hello。可以看到,一起建立了 go.mod 檔案。

實現一個簡單的方法

$ cat <<EOF > hello.go
package main

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
EOF
複製程式碼

我們建立了一個 hello.go 的檔案,並輸出呼叫的方法結果。

構建執行

$ go build   # 構建可執行檔案
$ ./hello  # 執行

Hello, world.  # 輸出結果
複製程式碼

執行構建之後,得到可執行檔案,我們執行了得到結果。 go.mod 檔案已更新為包含依賴項的顯式版本,其中 v1.5.2semver 標記:

$ cat go.mod

module github.com/keets2012/hello

require rsc.io/quote v1.5.2
複製程式碼

升級和降級依賴

應該使用 go get 來完成日常升級和降級依賴項,這將自動更新 go.mod 檔案。 或者可以直接編輯 go.mod

此外,像 go buildgo test 或甚至 go list 這樣的命令會根據需要自動新增新的依賴項以滿足匯入。

要檢視所有直接和間接依賴項的可用 minor 和 patch 程式升級:

go list -u -m all
複製程式碼

要升級到當前模組的所有直接和間接依賴關係的最新版本:

  • 執行 go get -u 以使用最新的次要版本或補丁版本
  • go -u = patch使用最新的補丁版本

要升級或降級到更具體的版本,go get 允許通過在 package 引數中新增@version 字尾或“模組查詢”來覆蓋版本選擇,例如 go get foo@v1.6.2go get foo @ e3702bed2,或者 go foo @'<v1.6.2'

semver

在上一小節,我們提到了 semver。golang 官方推薦的最佳實踐叫做 semver,這是一個簡稱,寫全了就是Semantic Versioning,也就是語義化版本。

語義化的定義

通俗地說,就是一種清晰可讀的,明確反應版本資訊的版本格式,更具體的規範在這裡。

如規範所言,形如 vX.Y.Z 的形式顯然比一串 hash 更直觀,所以 golang 的開發者才會把目光集中於此。

為何使用語義化版本

semver 簡化版本指定的作用是顯而易見的,然而僅此一條理由顯然有點缺乏說服力,通過semver對版本進行嚴格的約束,可以最大程度地保證向後相容以及避免 “breaking changes”,而這些都是 golang 所追求的。兩者一拍即合,所以 go modules 提供了語義化版本的支援。

如果你使用和釋出的包沒有版本 tag 或者處於 1.x 版本,那麼你可能體會不到什麼區別,因為 go mod 所支援的格式從始至終是遵循 semver 的,主要的區別體現在 v2.0.0 以及更高版本的包上。

“如果舊軟體包和新軟體包具有相同的匯入路徑,則新軟體包必須向後相容舊軟體包。” - go modules wiki

相同名字的物件應該向後相容,然而按照語義化版本的約定,當出現 v2.0.0 的時候一定表示發生了重大變化,很可能無法保證向後相容,這時候應該如何處理呢?

答案很簡單,我們為包的匯入路徑的末尾附加版本資訊即可,例如:

module my-module/v2

require (
  some/pkg/v2 v2.0.0
  some/pkg/v2/mod1 v2.0.0
  my/pkg/v3 v3.0.1
)
複製程式碼

格式總結為 pkgpath/vN,其中 N 是大於 1 的主要版本號。在程式碼裡匯入時也需要附帶上這個版本資訊,如import "some/pkg/v2"。這樣包的匯入路徑發生了變化,也不用擔心名稱相同的物件需要向後相容的限制了,因為 golang 認為不同的匯入路徑意味著不同的包。當然還有意外的情況:

  • 當使用gopkg.in格式時可以使用等價的require gopkg.in/some/pkg.v2 v2.0.0
  • 在版本資訊後加上 +incompatible 就可以不需要指定 /vN ,例如:require some/pkg v2.0.0+incompatible

除此以外的情況如果直接使用 v2+ 版本將會導致 go mod 報錯。

v2+ 版本的包允許和其他不同大版本的包同時存在(前提是新增了/vN),它們將被當做不同的包來處理。

另外 /vN 並不會影響你的倉庫,不需要建立一個v2對應的倉庫,這只是 go modules 新增的一種附加資訊而已。

當然如果你不想遵循這一規範或者需要相容現有程式碼,那麼指定 +incompatible 會是一個合理的選擇。不過 go modules 不推薦這種行為。

使用 vendor 目錄

如果你不喜歡 go mod 的快取方式,你可以使用 go mod vendor 回到 godep 或 govendor 使用的 vendor 目錄進行包管理的方式。

當然這個命令並不能讓你從godep之類的工具遷移到 go modules,它只是單純地把 go.sum 中的所有依賴下載到 vendor 目錄裡,如果你用它遷移 godep 你會發現 vendor 目錄裡的包會和 godep 指定的產生相當大的差異,所以請務必不要這樣做。

使用 go build -mod=vendor 來構建專案,因為在 go modules 模式下 go build 是遮蔽 vendor 機制的,所以需要特定引數重新開啟 vendor 機制:

go build -mod=vendor
./hello
hello world!
複製程式碼

構建成功。當釋出時也只需要和使用 godep 一樣將 vendor 目錄帶上即可。

總結

本文主要介紹了 go modules的一些特性和使用方法, go modules 是官方的包管理工具,Go 語言通過引入 module 的概念進而引入了 Go tool 的另外一種工作模式 module-aware mode 。在新的工作模式下,module 支援包依賴的版本化管理。

新的工作模式也帶來了一些問題,在大陸地區我們無法直接通過 go get 命令獲取到一些第三方包,這其中最常見的就是 golang.org/x 下面的各種優秀的包。一旦工作在模組下,go build 將不再關心 GOPATH 或是 vendor 下的包,而是到 GOPATH/pkg/mod 查詢是否有cache,如果沒有,則會去下載某個版本的 module,而對於某些包的 module,在大陸地區往往會失敗。我們將在下篇文章介紹 go module 的 proxy 配置實現。

推薦閱讀

Go的包管理工具

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Modules docs
  2. 再探go modules:使用與細節

相關文章