Go 部落格

使用 Go Modules

Tyler Bui-Palsulich 和 Eno Compton
2019 年 3 月 19 日

引言

本文是系列文章的第 1 部分。

注意:有關使用模組管理依賴的文件,請參閱管理依賴

Go 1.11 和 1.12 包含了對模組的初步支援,這是 Go 新的依賴管理系統,它使依賴版本資訊更明確且易於管理。這篇博文介紹了開始使用模組所需的基本操作。

模組是儲存在檔案樹中的Go 包的集合,根目錄下包含一個 go.mod 檔案。go.mod 檔案定義了模組的模組路徑(它也是根目錄的匯入路徑)及其依賴要求(成功構建所需的其他模組)。每個依賴要求都寫為模組路徑和一個特定的語義版本

從 Go 1.11 開始,噹噹前目錄或任何父目錄包含 go.mod 檔案時,go 命令啟用模組的使用,前提是該目錄不在 $GOPATH/src 內。(為了相容性,在 $GOPATH/src 內,即使找到了 go.mod 檔案,go 命令仍會以舊的 GOPATH 模式執行。詳情請參閱go 命令文件。) 從 Go 1.13 開始,模組模式將成為所有開發的預設設定。

本文將介紹使用模組開發 Go 程式碼時出現的常見操作序列:

  • 建立新模組。
  • 新增依賴。
  • 升級依賴。
  • 新增對新主要版本的依賴。
  • 將依賴升級到新的主要版本。
  • 移除未使用的依賴。

建立新模組

讓我們建立一個新模組。

$GOPATH/src 以外的某個地方建立一個新的空目錄,cd 進入該目錄,然後建立一個新的原始檔 hello.go

package hello

func Hello() string {
    return "Hello, world."
}

讓我們也在 hello_test.go 中編寫一個測試:

package hello

import "testing"

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

此時,該目錄包含一個包,但不是一個模組,因為它沒有 go.mod 檔案。如果我們當前在 /home/gopher/hello 目錄並執行 go test,我們會看到:

$ go test
PASS
ok      _/home/gopher/hello 0.020s
$

最後一行總結了整個包測試。由於我們在 $GOPATH 之外工作,並且也沒有在任何模組內工作,go 命令不知道當前目錄的匯入路徑,因此根據目錄名生成了一個假的匯入路徑:_/home/gopher/hello

讓我們使用 go mod init 命令將當前目錄設定為模組的根目錄,然後再次嘗試 go test

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok      example.com/hello   0.020s
$

恭喜!您已經編寫並測試了您的第一個模組。

go mod init 命令寫入了一個 go.mod 檔案:

$ cat go.mod
module example.com/hello

go 1.12
$

go.mod 檔案只出現在模組的根目錄中。子目錄中的包的匯入路徑由模組路徑加上子目錄路徑組成。例如,如果我們建立了一個子目錄 world,我們不需要(也不想)在那裡執行 go mod init。該包將被自動識別為 example.com/hello 模組的一部分,匯入路徑為 example.com/hello/world

新增依賴

Go modules 的主要動機是改善使用(即新增依賴於)其他開發者編寫的程式碼的體驗。

讓我們更新 hello.go 以匯入 rsc.io/quote 並使用它來實現 Hello 函式:

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

現在讓我們再次執行測試:

$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok      example.com/hello   0.023s
$

go 命令使用 go.mod 中列出的特定依賴模組版本來解析匯入。當遇到 go.mod 中的任何模組都未提供的包匯入時,go 命令會自動查詢包含該包的模組,並使用最新版本將其新增到 go.mod 中。(“最新”版本定義為最新的穩定版(非預釋出版)標記版本,否則是最新的預釋出標記版本,否則是最新未標記版本。)在我們的示例中,go test 將新的匯入 rsc.io/quote 解析到模組 rsc.io/quote v1.5.2。它還下載了 rsc.io/quote 使用的兩個依賴,即 rsc.io/samplergolang.org/x/textgo.mod 檔案中只記錄直接依賴:

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

第二次執行 go test 命令不會重複此工作,因為 go.mod 檔案現在已更新,並且下載的模組已在本地快取(在 $GOPATH/pkg/mod 中):

$ go test
PASS
ok      example.com/hello   0.020s
$

請注意,雖然 go 命令使新增新依賴快速且簡單,但這並非沒有代價。您的模組現在確實依賴於新依賴,這在正確性、安全性和適當的許可等方面至關重要,這僅僅是其中幾點。有關更多考量,請參閱 Russ Cox 的博文:“我們的軟體依賴問題”。

正如我們在上面看到的,新增一個直接依賴通常也會引入其他間接依賴。命令 go list -m all 列出當前模組及其所有依賴:

$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$

go list 輸出中,當前模組(也稱為主模組)始終是第一行,後面是按模組路徑排序的依賴。

golang.org/x/text 版本 v0.0.0-20170915032832-14c0d48ead0c 是一個偽版本的示例,它是 go 命令用於特定未標記提交的版本語法。

除了 go.modgo 命令還維護一個名為 go.sum 的檔案,其中包含特定模組版本內容的預期加密雜湊

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$

go 命令使用 go.sum 檔案來確保將來下載這些模組時檢索到的內容與首次下載時相同,以確保您的專案依賴的模組不會意外更改,無論是出於惡意、意外還是其他原因。go.modgo.sum 都應該納入版本控制。

升級依賴

使用 Go modules,版本透過語義版本標記引用。語義版本有三個部分:主要版本、次要版本和補丁版本。例如,對於 v0.1.2,主要版本是 0,次要版本是 1,補丁版本是 2。讓我們來看幾個次要版本升級。在下一節中,我們將考慮主要版本升級。

go list -m all 的輸出中,我們可以看到我們正在使用 golang.org/x/text 的一個未標記版本。讓我們升級到最新的標記版本並測試一切是否仍然正常工作:

$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok      example.com/hello   0.013s
$

耶!所有測試都通過了。讓我們再看一下 go list -m all 的輸出和 go.mod 檔案:

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
)
$

golang.org/x/text 包已升級到最新的標記版本 (v0.3.0)。go.mod 檔案也已更新以指定 v0.3.0indirect 註釋表示此依賴項不是直接由該模組使用,而是僅透過其他模組依賴項間接使用。有關詳細資訊,請參閱 go help modules

現在讓我們嘗試升級 rsc.io/sampler 的次要版本。以同樣的方式開始,執行 go get 並執行測試:

$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
    hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL    example.com/hello   0.014s
$

哦,糟糕!測試失敗表明 rsc.io/sampler 的最新版本與我們的用法不相容。讓我們列出該模組可用的標記版本:

$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$

我們之前使用的是 v1.3.0;v1.99.99 顯然不行。也許我們可以嘗試使用 v1.3.1:

$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok      example.com/hello   0.022s
$

請注意 go get 引數中的顯式 @v1.3.1。通常,傳遞給 go get 的每個引數都可以帶一個顯式版本;預設是 @latest,它會解析到前面定義的最新版本。

新增對新主要版本的依賴

讓我們向我們的包新增一個新函式:func Proverb 透過呼叫 quote.Concurrency 返回一個 Go 併發諺語,該函式由模組 rsc.io/quote/v3 提供。首先我們更新 hello.go 新增新函式:

package hello

import (
    "rsc.io/quote"
    quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
    return quote.Hello()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

然後我們在 hello_test.go 中新增一個測試:

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

然後我們可以測試我們的程式碼:

$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok      example.com/hello   0.024s
$

請注意,我們的模組現在同時依賴於 rsc.io/quotersc.io/quote/v3

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

Go 模組的每個不同主要版本(v1v2 等)都使用不同的模組路徑:從 v2 開始,路徑必須以主要版本結尾。在示例中,rsc.io/quotev3 不再是 rsc.io/quote:相反,它由模組路徑 rsc.io/quote/v3 標識。這種約定稱為語義匯入版本控制,它為不相容的包(具有不同主要版本的包)賦予不同的名稱。相比之下,rsc.io/quotev1.6.0 應該向後相容 v1.5.2,因此它重用了名稱 rsc.io/quote。(在上一節中,rsc.io/samplerv1.99.99 應該rsc.io/samplerv1.3.0 向後相容,但錯誤或客戶端對模組行為的不正確假設都可能發生。)

go 命令允許構建中最多包含特定模組路徑的一個版本,這意味著每個主要版本最多一個:一個 rsc.io/quote、一個 rsc.io/quote/v2、一個 rsc.io/quote/v3,等等。這為模組作者提供了一個關於單個模組路徑可能重複的明確規則:程式不可能同時使用 rsc.io/quote v1.5.2rsc.io/quote v1.6.0 進行構建。同時,允許模組的不同主要版本(因為它們具有不同的路徑)使模組使用者能夠增量地升級到新的主要版本。在此示例中,我們想使用 rsc/quote/v3 v3.1.0 中的 quote.Concurrency,但尚未準備好遷移我們對 rsc.io/quote v1.5.2 的使用。增量遷移的能力在大程式或程式碼庫中尤為重要。

將依賴升級到新的主要版本

讓我們完成從使用 rsc.io/quote 到僅使用 rsc.io/quote/v3 的轉換。由於主要版本發生了變化,我們應該預期一些 API 可能已被移除、重新命名或以不相容的方式更改。閱讀文件,我們可以看到 Hello 已變為 HelloV3

$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$

我們可以更新 hello.go 中對 quote.Hello() 的使用,改為使用 quoteV3.HelloV3()

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
    return quoteV3.HelloV3()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

此時,不再需要重新命名的匯入,因此我們可以撤銷它:

package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

讓我們重新執行測試,確保一切正常:

$ go test
PASS
ok      example.com/hello       0.014s

移除未使用的依賴

我們已經移除了所有對 rsc.io/quote 的使用,但它仍然顯示在 go list -m all 的輸出和我們的 go.mod 檔案中:

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.0.0
    rsc.io/sampler v1.3.1 // indirect
)
$

為什麼?因為構建單個包,比如使用 go buildgo test,可以很容易地知道何時缺少某些東西需要新增,但無法知道何時可以安全地移除某些東西。只有在檢查了模組中的所有包以及這些包的所有可能的構建標籤組合後,才能移除依賴。普通的構建命令不會載入此資訊,因此無法安全地移除依賴。

go mod tidy 命令會清理這些未使用的依賴:

$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote/v3 v3.1.0
    rsc.io/sampler v1.3.1 // indirect
)

$ go test
PASS
ok      example.com/hello   0.020s
$

結論

Go modules 是 Go 依賴管理的未來。模組功能現在已在所有受支援的 Go 版本中可用(即在 Go 1.11 和 Go 1.12 中)。

本文介紹了使用 Go modules 的以下工作流程:

  • go mod init 建立一個新的模組,初始化描述該模組的 go.mod 檔案。
  • go buildgo test 和其他構建包的命令會在需要時向 go.mod 新增新的依賴。
  • go list -m all 列印當前模組的依賴。
  • go get 更改依賴的所需版本(或新增新的依賴)。
  • go mod tidy 移除未使用的依賴。

我們鼓勵您在本地開發中開始使用模組,並將 go.modgo.sum 檔案新增到您的專案中。為了提供反饋並幫助塑造 Go 依賴管理的未來,請向我們傳送錯誤報告經驗報告

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

下一篇文章:除錯 Go 1.12 中部署的內容
上一篇文章:新的 Go 開發者網路
部落格索引