Go 部落格

使用 Go 模組

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

引言

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

注意:如需模組相依性管理文件,請參閱 管理相依性

Go 1.11 和 1.12 含初步的 模組支援,Go 新的相依性管理系統 可明確說明相依性版本資訊,並更容易管理。這篇部落格文章是使用模組所需基本運作的介紹。

模組是一群以檔案樹儲存在具有根目錄中的 `go.mod` 檔案中的 Go 套件。`go.mod` 檔案定義模組的模組路徑,這也是用於根目錄的匯入路徑,以及它的相依性需求,是成功建置所需的其它模組。每個相依性需求寫作模組路徑和特定 語意版本

自 Go 1.11 起,go 指令可以在目前目錄或任何父目錄具有 `go.mod` 時使用模組,只要目錄 `$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` 檔案。假設我們在 `go test` 處理 `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,並使用最新版本。(「最新版本」定義為標記的最新穩定版本(非pre-release 版本),或標記的最新 pre-release 版本,或標記的最新未標記版本。)在我們的範例中,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.mod 之外,go 指令會管理一個名為 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 透過呼叫由模組 rsc.io/quote/v3 提供的 quote.Concurrency,傳回 Go 並行處理諺語。首先,我們更新 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.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 模組是 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 檔案新增到您的專案。若要提供回饋並協助 shape Go 依賴項管理的未來,請寄發 錯誤報告使用體驗報告 給我們。

謝謝您提供的所有回饋,並協助改善模組功能。

下一篇:在 Go 1.12 中除錯佈署內容
上一篇:嶄新的 Go Developer Network
部落格索引