Go的1.11和1.12版本包括對模組--新的Go依賴管理系統的初步支援,使依賴版本資訊變得明確且更易於管理。這篇部落格文章介紹了開始使用模組所需的基本操作。
模組是儲存在根目錄有一個 go.mod
檔案的檔案樹中的 Go 包(package)的集合。go.mod
檔案定義了模組的module path(也是模組根目錄的匯入路徑)以及模組依賴的其他模組的要求,滿足了依賴要求模組才能被成功構建起來。每個依賴模組的要求被寫為一個模組路徑和相應的模組版本。
下面展示了一個簡單的go.mod
檔案
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
從Go 1.11開始,噹噹前目錄或任何父目錄有go.mod
時,只要該目錄位於$GOPATH/src
之外,go命令就可以使用模組。 (在$ GOPATH/src
內部,出於相容性考慮,即使找到了go.mod
,go命令仍然在舊的GOPATH模式下執行。)從Go 1.13開始,模組模式將是所有開發的預設模式。
本文介紹了使用模組開發Go程式碼時出現的一系列常見操作:
- 建立一個新模組。
- 新增模組的依賴項。
- 升級模組的依賴項。
- 增加依賴項的主版本。
- 將依賴項升級到新的主版本。
- 刪除未使用的依賴項。
建立一個新模組
在$GOPATH/src
之外的某個地方建立一個新的空目錄,然後在新目錄下建立一個新的原始檔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)
}
}
假設我們新建的目錄為/home/gopher/hello
,此時該目錄包含一個包,而不是模組,因為目錄中沒有go.mod
檔案。使用 go 命令執行測試會看到:
$ 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 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 test
其執行結果如下:
$ go test
PASS
ok example.com/hello 0.020s
$
現在輸出中的匯入路徑變成了example.com/hello
,不知不覺中就編寫並測試了我們的第一個go模組。
新增模組依賴
Go模組的主要動機是改善管理使用其他開發者編寫的程式碼(程式碼依賴)的體驗。 讓我們更新hello.go
以匯入rsc.io/quote
並使用它來實現Hello
函式:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
現在再次執行go test
:
$ 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/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 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
的輸出中,當前模組也被稱為主模組,總是會出現在第一行,後面跟隨的是根據模組路徑排序後展示的依賴項:
除了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.sum
並不是類似package-lock.json
的包管理器鎖檔案,它是一個構建狀態跟蹤檔案。它會記錄當前模組所有的直接和間接依賴,以及這些依賴的校驗和,從而提供一個可以100%復現的構建過程並對構建物件提供安全性的保證。所以應該將go.mod
和go.sum
都新增到版本控制中。go.sum
同時還會保留過去使用的包的版本資訊,以便日後可能的版本回退,這一點也與普通的鎖檔案不同。所以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
檔案中golang.org/x/text
也已更新為指定的v0.3.0
。indirect
註釋指明依賴項不被當前模組直接使用,而是由其依賴的模組所使用的。
現在,讓我們嘗試升級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
$
我們將 rsc.io/sampler
升級到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。通常,傳遞給get的每個引數都可以採用顯式形式。預設值為@latest,它將解析為先前定義的最新版本。
增加依賴的主版本
讓我們在包中新增一個新函式:函式Proverb
通過呼叫quote.Concurrency
返回Go併發諺語(就是Pike說在某年 Go 開發大會上說的金句:"Concurrency is not parallelism"),這是由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
$
可以看到 go 命令下載安裝了rsc.io/quote/v3
模組,現在我們的模組同時依賴了 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
。
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"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$
我們可以把hello.go
中對qoute.Hello()
的呼叫更新為使用quoteV3.HelloV3()
,現在已經不需要對 v3 版本的匯入路徑重新命名了所以我們撤銷包的重新命名(注意預設包名不會包含版本字尾)。
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)可以輕鬆判斷出來缺少某些內容並需要新增,但無法確定某些內容是否可以安全刪除。只有在檢查模組中的所有軟體包以及這些軟體包的所有可能的構建標記組合之後,才能刪除依賴項。普通的build
命令不會載入此資訊,因此它不能安全地刪除依賴項。
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依賴管理的未來。從 Go1.11都提供模組功能。 這篇文章介紹了使用Go模組的這些工作流程:
-
go mod init 建立一個新模組,初始化描述它的go.mod檔案。
-
go buil,go test和其他程式包構建命令根據需要向
go.mod
新增新的依賴項。 -
go list -m all列印當前模組的依賴關係。
-
go get更改所需依賴的版本(或新增新的依賴)。
-
go mod tidy刪除未使用的依賴項。
參考文章:https://blog.golang.org/using-go-modules
現在越來越多的專案都開始用Go Modules來管理依賴包,我也是剛開始嘗試將現有專案遷移到用 Go Modules管理的模式,在實踐中發現還有很多要學習的地方,後期會分享更多這方面的學習文章和總結。
本作品採用《CC 協議》,轉載必須註明作者和本文連結