Go 部落格

遷移到 Go 模組

Jean de Klerk
2019 年 8 月 21 日

引言

這篇文章是系列文章的第 2 部分。

注意:有關文件,請參閱 管理相依性開發和發布模組

Go 專案使用了各種相依性管理策略。例如 供應 工具 depglide 很受歡迎,但它們的行為有很大的不同,而且並非總是能很好地協同工作。有些專案會將其整個 GOPATH 目錄儲存在單一 Git 存放庫中。其他專案則僅依賴 go get 並預期將最近版本的相依性安裝在 GOPATH 中。

Go 的模組系統於 Go 1.11 中推出,提供內建到 go 命令的官方相依性管理解決方案。本文說明將專案轉換到模組的工具和技術

請注意:如果您的專案已標記為 v2.0.0 或更高版本,您在新增 go.mod 檔案時需要更新您的模組路徑。我們將在未來專注於 v2 和更高版本的文章中說明如何執行此動作,而不會影響您的使用者。

在您的專案中移轉到 Go 模組

專案在開始轉換到 Go 模組時可能處於三種狀態之一

  • 全新的 Go 專案。
  • 已建立的 Go 專案,使用非模組相依性管理員。
  • 已建立的 Go 專案,沒有任何相依性管理員。

第一個案例已在 使用 Go 模組 中涵蓋;我們將在此篇文章中處理後兩者。

使用相依性管理員

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

$ 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 模組後,某些測試可能需要調整。

如果程式需要在套件目錄中寫入檔案,當套件目錄位於唯讀的模組快取中時,可能會失敗。尤其是,這可能會導致 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 模組 以了解如何增加和發布版本。

輸入和正規模組路徑

每個模組在其 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 "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 模組對於大多數使用者來說應該是一個簡單的流程。偶爾會因為非規範匯入路徑或依存項內的重大變更而產生問題。後續的貼文將探討 發布新版本、v2 及之後的版本,以及除錯特殊情況的方法。

如要提供意見回饋並協助塑造 Go 中依賴項管理的未來,請向我們傳送 錯誤報告經驗報告

感謝各位提供所有意見回饋並協助改善模組。

下一篇文章:模組鏡像和檢查碼資料庫已推出
前一篇文章:合作夥伴高峰會 2019
部落格索引