Go 部落格

遷移到 Go Modules

Jean Barkhuysen
2019 年 8 月 21 日

引言

此文章是系列文章的第 2 部分。

注意: 有關文件,請參閱管理依賴項開發和釋出模組

Go 專案使用多種依賴管理策略。Vendoring 工具,例如 depglide 很受歡迎,但它們的行為差異很大,並且並非總是能很好地協同工作。有些專案將其整個 GOPATH 目錄儲存在一個 Git 倉庫中。另一些專案則只依賴於 go get,並期望將相當新的依賴項版本安裝到 GOPATH 中。

Go 模組系統於 Go 1.11 中引入,提供了內置於 go 命令中的官方依賴管理解決方案。本文介紹了將專案轉換為模組的工具和技術。

請注意:如果您的專案已標記為 v2.0.0 或更高版本,則在新增 go.mod 檔案時,您需要更新模組路徑。我們將在未來一篇關於 v2 及更高版本的文章中解釋如何在不破壞使用者的情況下做到這一點。

在您的專案中遷移到 Go modules

專案在開始過渡到 Go 模組時可能有以下三種狀態之一

  • 一個全新的 Go 專案。
  • 一個使用非模組依賴管理器的成熟 Go 專案。
  • 一個沒有任何依賴管理器的成熟 Go 專案。

第一種情況已在使用 Go Modules中介紹;我們將在本文中討論後兩種情況。

使用依賴管理器

要轉換已經使用依賴管理工具的專案,請執行以下命令

$ git clone https://github.com/my/project
[...]
$ cd project
$ cat Godeps/Godeps.json
{
    "ImportPath": "github.com/my/project",
    "GoVersion": "go1.12",
    "GodepVersion": "v80",
    "Deps": [
        {
            "ImportPath": "rsc.io/binaryregexp",
            "Comment": "v0.2.0-1-g545cabd",
            "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
        },
        {
            "ImportPath": "rsc.io/binaryregexp/syntax",
            "Comment": "v0.2.0-1-g545cabd",
            "Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
        }
    ]
}
$ go mod init github.com/my/project
go: creating new go.mod: module github.com/my/project
go: copying requirements from Godeps/Godeps.json
$ cat go.mod
module github.com/my/project

go 1.12

require rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$

go mod init 會建立一個新的 go.mod 檔案,並自動從 Godeps.jsonGopkg.lock 或許多其他支援的格式中匯入依賴項。go mod init 的引數是模組路徑,即模組可能存在的位置。

此時最好暫停,在繼續之前執行 go build ./...go test ./...。後續步驟可能會修改您的 go.mod 檔案,因此如果您喜歡迭代方法,這是您的 go.mod 檔案最接近您預模組依賴項規範的狀態。

$ go mod tidy
go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$ cat go.sum
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca h1:FKXXXJ6G2bFoVe7hX3kEX6Izxw5ZKRH57DFBJmHCbkU=
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
$

go mod tidy 會查詢模組中包間接匯入的所有包。它會為任何已知模組未提供的包新增新的模組要求,並刪除對未提供任何匯入包的模組的要求。如果一個模組提供的包只被尚未遷移到模組的專案匯入,則模組要求將標記有 // indirect 註釋。在將 go.mod 檔案提交到版本控制之前,執行 go mod tidy 始終是一個好習慣。

讓我們透過確保程式碼構建並透過測試來完成此操作

$ go build ./...
$ go test ./...
[...]
$

請注意,其他依賴項管理器可能會在單個包或整個倉庫(而不是模組)級別指定依賴項,並且通常不識別依賴項的 go.mod 檔案中指定的要​​求。因此,您可能無法獲得與以前完全相同的每個包版本,並且存在升級超過重大更改的風險。因此,在執行上述命令後對結果依賴項進行審計非常重要。為此,請執行

$ go list -m all
go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
github.com/my/project
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$

並將其結果版本與舊的依賴管理檔案進行比較,以確保所選版本是合適的。如果您發現某個版本不是您想要的,您可以使用 go mod why -m 和/或 go mod graph 找出原因,並使用 go get 升級或降級到正確的版本。(如果請求的版本早於先前選擇的版本,go get 將根據需要降級其他依賴項以保持相容性。) 例如,

$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/binaryregexp@v0.2.0
$

沒有依賴管理器

對於沒有依賴管理系統的 Go 專案,首先建立一個 go.mod 檔案

$ git clone https://go.googlesource.com/blog
[...]
$ cd blog
$ go mod init golang.org/x/blog
go: creating new go.mod: module golang.org/x/blog
$ cat go.mod
module golang.org/x/blog

go 1.12
$

如果沒有來自先前的依賴管理器的配置檔案,go mod init 將建立一個僅包含 modulego 指令的 go.mod 檔案。在這個例子中,我們將模組路徑設定為 golang.org/x/blog,因為這是它的自定義匯入路徑。使用者可以使用此路徑匯入包,我們必須小心不要更改它。

module 指令宣告模組路徑,go 指令宣告用於編譯模組內程式碼的 Go 語言的預期版本。

接下來,執行 go mod tidy 以新增模組的依賴項

$ go mod tidy
go: finding golang.org/x/website latest
go: finding gopkg.in/tomb.v2 latest
go: finding golang.org/x/net latest
go: finding golang.org/x/tools latest
go: downloading github.com/gorilla/context v1.1.1
go: downloading golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: extracting github.com/gorilla/context v1.1.1
go: extracting golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: downloading gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
go: extracting golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
$ cat go.mod
module golang.org/x/blog

go 1.12

require (
    github.com/gorilla/context v1.1.1
    golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
    golang.org/x/text v0.3.2
    golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
    golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
    gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
)
$ cat go.sum
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
[...]
$

go mod tidy 為模組中包間接匯入的所有包添加了模組要求,並使用每個庫在特定版本上的校驗和構建了 go.sum。讓我們透過確保程式碼仍然構建並透過測試來完成此操作

$ go build ./...
$ go test ./...
ok      golang.org/x/blog   0.335s
?       golang.org/x/blog/content/appengine [no test files]
ok      golang.org/x/blog/content/cover 0.040s
?       golang.org/x/blog/content/h2push/server [no test files]
?       golang.org/x/blog/content/survey2016    [no test files]
?       golang.org/x/blog/content/survey2017    [no test files]
?       golang.org/x/blog/support/racy  [no test files]
$

請注意,當 go mod tidy 新增一個要求時,它會新增模組的最新版本。如果您的 GOPATH 中包含某個依賴項的舊版本,而該依賴項隨後釋出了重大更改,您可能會在 go mod tidygo buildgo test 中看到錯誤。如果發生這種情況,請嘗試使用 go get 降級到舊版本(例如,go get github.com/broken/module@v1.1.0),或者花時間使您的模組與每個依賴項的最新版本相容。

模組模式下的測試

遷移到 Go modules 後,某些測試可能需要調整。

如果測試需要在包目錄中寫入檔案,當包目錄位於模組快取中時(只讀),測試可能會失敗。特別是,這可能會導致 go test all 失敗。測試應將其需要寫入的檔案複製到臨時目錄中。

如果測試依賴相對路徑(../package-in-another-module)來查詢和讀取另一個包中的檔案,那麼如果該包位於另一個模組中,它將會失敗。該模組將位於模組快取的版本子目錄中,或位於 replace 指令中指定的路徑。如果是這種情況,您可能需要將測試輸入複製到您的模組中,或者將測試輸入從原始檔案轉換為嵌入到 .go 原始檔中的資料。

如果測試期望測試中的 go 命令在 GOPATH 模式下執行,則它可能會失敗。如果是這種情況,您可能需要向待測試的原始碼樹新增 go.mod 檔案,或者顯式設定 GO111MODULE=off

釋出版本

最後,您應該為您的新模組打標籤併發佈一個版本。如果您尚未釋出任何版本,這是可選的,但如果沒有官方版本,下游使用者將依賴使用偽版本的特定提交,這可能更難以支援。

$ git tag v1.2.0
$ git push origin v1.2.0

您的新 go.mod 檔案定義了模組的規範匯入路徑並添加了新的最低版本要求。如果您的使用者已經使用正確的匯入路徑,並且您的依賴項沒有進行破壞性更改,那麼新增 go.mod 檔案是向後相容的——但這會是一個重大更改,並且可能會暴露現有問題。如果您有現有的版本標籤,您應該增加次要版本。請參閱釋出 Go Modules 以瞭解如何增加和釋出版本。

匯入和規範模組路徑

每個模組都在其 go.mod 檔案中宣告其模組路徑。每個引用模組內包的 import 語句都必須以模組路徑作為包路徑的字首。然而,go 命令可能會透過許多不同的遠端匯入路徑遇到包含模組的倉庫。例如,golang.org/x/lintgithub.com/golang/lint 都解析為包含託管在go.googlesource.com/lint上的程式碼的倉庫。該倉庫中包含的go.mod 檔案宣告其路徑為 golang.org/x/lint,因此只有該路徑對應於有效的模組。

Go 1.4 提供了一種使用// import 註釋宣告規範匯入路徑的機制,但包作者並非總是提供它們。因此,在模組之前編寫的程式碼可能使用了模組的非規範匯入路徑,而沒有因不匹配而出現錯誤。在使用模組時,匯入路徑必須與規範模組路徑匹配,因此您可能需要更新 import 語句:例如,您可能需要將 import "github.com/golang/lint" 更改為 import "golang.org/x/lint"

模組的規範路徑可能與其倉庫路徑不同的另一種情況發生在主要版本為 2 或更高的 Go 模組上。主要版本高於 1 的 Go 模組必須在其模組路徑中包含主要版本字尾:例如,版本 v2.0.0 必須具有後綴 /v2。但是,import 語句可能引用了模組內的包沒有該字尾。例如,github.com/russross/blackfriday/v2 的非模組使用者在 v2.0.1 版本時可能將其匯入為 github.com/russross/blackfriday,並且需要更新匯入路徑以包含 /v2 字尾。

結論

對於大多數使用者來說,轉換為 Go modules 應該是一個簡單的過程。偶爾可能會因非規範匯入路徑或依賴項中的破壞性更改而出現問題。未來的文章將探討釋出新版本、v2 及更高版本,以及除錯異常情況的方法。

為了提供反饋並幫助塑造 Go 中依賴管理的未來,請向我們傳送錯誤報告經驗報告

感謝您的所有反饋和對改進模組的幫助。

下一篇文章:模組映象和校驗和資料庫已啟動
上一篇文章:2019 年貢獻者峰會
部落格索引