Go 部落格

使用 Go 模組

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

引言

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

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

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

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

從 Go 1.11 開始,噹噹前目錄或任何父目錄存在 go.mod 檔案時,go 命令會啟用模組的使用,前提是該目錄不在 $GOPATH/src 中。(在 $GOPATH/src 內部,為了相容性,go 命令仍然在舊的 GOPATH 模式下執行,即使找到了 go.mod。詳情請參閱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 模組的主要動機是改善使用(即新增對)其他開發人員編寫的程式碼的體驗。

讓我們更新 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 中任何模組未提供的包的 import 時,go 命令會自動查詢包含該包的模組並將其新增到 go.mod 中,使用最新版本。(“最新”被定義為最新的已標記穩定(非預釋出)版本,否則為最新的已標記預釋出版本,否則為最新的未標記版本。)在我們的示例中,go test 將新的匯入 rsc.io/quote 解析為模組 rsc.io/quote v1.5.2。它還下載了 rsc.io/quote 使用的兩個依賴項,即 rsc.io/samplergolang.org/x/text。只有直接依賴項記錄在 go.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 模組,版本透過語義版本標籤引用。語義版本有三個部分:主版本、次版本和補丁版本。例如,對於 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 allgo.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/sampler v1.99.99 應該向後相容 rsc.io/sampler v1.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.goquote.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 模組是 Go 中依賴管理的未來。模組功能現在在所有受支援的 Go 版本中都可用(即在 Go 1.11 和 Go 1.12 中)。

這篇文章介紹了這些使用 Go 模組的工作流程

  • 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 開發者網路
部落格索引