Go 部落格
使用 Go 模組
引言
這篇文章是系列文章的第一部分。
- 第 1 部分 — 使用 Go 模組(本篇文章)
- 第 2 部分 — 移至 Go 模組
- 第 3 部分 — 發行 Go 模組
- 第 4 部分 — Go 模組:v2 及更新版本
- 第 5 部分 — 維持模組相容性
注意:如需模組相依性管理文件,請參閱 管理相依性。
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/sampler
和 golang.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.mod
和 go.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 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.0
。indirect
註解表示套件並未直接使用這個模組,而僅透過其他模組依賴項間接使用。有關詳細資訊,請參閱 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/quote
和 rsc.io/quote/v3
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$
每個不同的 Go 模組主版本(v1
、v2
等)都會使用不同的模組路徑:從 v2
開始,路徑必須以主版本結束。在範例中,rsc.io/quote
的 v3
不再是 rsc.io/quote
:它改由模組路徑 rsc.io/quote/v3
來識別。此慣例稱為 語意化匯入版本控管,它會針對不相容套件(具有不同主版本者)提供不同的名稱。相較之下,rsc.io/quote
的 v1.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.2
和 rsc.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 build
或 go 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 build
、go test
和其他套件建構指令會根據需要將新的依賴項新增到go.mod
。go list -m all
會列印目前模組的依賴項。go get
會變更依賴項的必要版本(或新增新的依賴項)。go mod tidy
會移除未使用的依賴項。
我們鼓勵您開始在您的本機開發中使用模組,並將 go.mod
和 go.sum
檔案新增到您的專案。若要提供回饋並協助 shape Go 依賴項管理的未來,請寄發 錯誤報告 或 使用體驗報告 給我們。
謝謝您提供的所有回饋,並協助改善模組功能。