1 速覽
在正式了解Golang Modules之前,我們先速覽一下其使用方式。
在$GOPATH之外的任意地方,創建一個文件夾:
$ mkdir -p /tmp/hello $ cd /tmp/hello
然后初始化一個新的Module:
$ go mod init github.com/olzhy/hello
輸出:
go: creating new go.mod: module github.com/olzhy/hello
go.mod內容為:
module github.com/olzhy/hello go 1.12
然后寫一段代碼:
$ cat < hello.go package main import ( "fmt" "github.com/olzhy/quote" ) func main() { fmt.Println(quote.Hello()) } EOF
build一下:
$ go build go: finding github.com/olzhy/quote latest go: downloading github.com/olzhy/quote v0.0.0-20190510033103-5cb7d4598cfa go: extracting github.com/olzhy/quote v0.0.0-20190510033103-5cb7d4598cfa
go.mod內容為:
module github.com/olzhy/hello go 1.12 require github.com/olzhy/quote v0.0.0-20190510033103-5cb7d4598cfa
可以看到,其會從https://github.com/olzhy/quote master分支拉取最新提交5cb7d4598cfa。
該依賴工程非Module管理模式,其僅有兩個文件:
hello.go README.md
現在給依賴工程打一個TAG,名為v1.0.0。
然后hello工程更新依賴:
$ go get -u go: finding github.com/olzhy/quote v1.0.0 go: downloading github.com/olzhy/quote v1.0.0 go: extracting github.com/olzhy/quote v1.0.0
查看go.mod內容為:
module github.com/olzhy/hello go 1.12 require github.com/olzhy/quote v1.0.0
如下為使用Golang Module后的日常工作流。
每日工作流:
- a)源碼根據需要加入包引入語句;
- b)標準命令,如go build及go test等會自動更新go.mod并下載依賴包;
- c)當需要特定版本時,可以使用諸如go get [email protected],go get foo@master,go get foo@e3702bed2命令或直接編輯go.mod文件。
其它通用命令:
- a)go list -m all 查看一次構建使用的直接及間接依賴的最終版本;
- b)go list -u -m all 查看直接及間接依賴的可用的小版本或補丁版本更新;
- c)go get -u 或 go get -u=patch 將直接與間接依賴更新為最新小版本或補丁版本;
- d)go build ./... 或 go test ./... 構建或測試模塊中的所有包;
- e)go mod tidy 從go.mod清理不再使用的包;
- f)go mod edit -replace [email protected]=../foo 替換依賴為本地復制或指定版本;
- g)go mod vendor 轉換為vendor依賴方式。
2 概念
2.1 模塊
Module是一組相關Go package的集合,其作為一個單獨的單元來版本化。
Module記錄精確的依賴項,提供可重復的構建。
通常,一個版本控制倉庫僅包含一個Module(支持單倉庫多Module,但其會比單倉庫單Module復雜很多)。
倉庫、模塊與包的關系:
- a)一個倉庫包含一個或多個模塊;
- b)一個模塊包含一個或多個包;
- c)一個包在一個文件夾包含一個或多個.go文件。
模塊必須以語義學版本命名,格式為v(主版本).(小版本).(補丁),諸如v0.1.0、v1.2.3,v1.5.0-rc.1等。
模塊由一組源文件樹在根目錄定義一個go.mod文件,模塊源碼可以位于GOPATH之外,有如下原語module,require,replace,exclude。
如下為github.com/olzhy/hello模塊的go.mod文件示例內容:
module github.com/olzhy/hello require ( github.com/some/dependency v1.2.3 github.com/another/dependency/v4 v4.0.0 )
可以看到,一個模塊通過module原語聲明模塊ID,其標識模塊路徑。該模塊下某一個包的被引用路徑由該模塊路徑與自go.mod所在路徑起一直到包的路徑止的相對路徑共同決定。
如,一個模塊在go.mod聲明其ID為example.com/my/module,那么引用該模塊下mypkg包的代碼為:
import "example.com/my/module/mypkg"
2.2 版本選擇
若源碼中增加了go.mod中未require的新的依賴包,絕大多數諸如go build,go test命令會自動找到對應的包并在go.mod中采用require原語加入該直接依賴的最高版本。
例如,您依賴的模塊M的帶標簽的發布版本為v1.2.3,那么您的go.mod會新加入require M v1.2.3這一行語句,意味著依賴模塊M所允許的版本>= v1.2.3并< v2(v2被認為與v1不兼容)。 最小版本選擇算法用于對一次構建的所有模塊選擇版本,對于每個模塊,采用該算法選擇的版本為語義學最高版本。
下面舉個例子:
若您依賴的模塊A依賴D(require D v1.0.0),而您依賴的模塊B同樣依賴D(require D v1.1.1),然后,最小版本選擇(選擇最高的版本)將會選擇v1.1.1版本的D。而選擇v1.1.1版本的D是一致的,即使將來發布了v1.2.0版本的D。
這樣即可保持100%可重復構建。當然您也可以手動升級D為最新可用版本或者指定其為其它版本。
查看所選模塊版本列表(包括間接依賴),可以使用:
go list -m all
2.3 語義學版本引用
Go多年來推薦的包版本化方式:
開放使用的包在演進時應保持向后兼容的準則,Go 1兼容性準則即是一個好的參考。不要移除已導出的名稱,若要加一個新功能,需加一個新接口,不要改動老接口名。實在需要推倒之前的,請創建一個新包,以新的路徑而被引用。
最后一句很重要,若破壞了兼容性,需更改包的引用路徑。
對Go 1.11模塊而言,引用兼容性準則可以概述為:
若新包沿用舊包的引用路徑,新包必須向后兼容舊包。
參考語義學版本命名規則,當一個原始為v1或v1以上的包發生了不兼容變更,該包需要更改主版本。
所以,根據引用兼容性準則及語義學版本命名規則(合稱為語義學版本引用),主版本需要包含在引用路徑內。這樣即可保障不兼容的主版本升級時,引用路徑即會改變。
根據語義學版本引用規則,選用Go Module的代碼必須遵守如下規則:
- a)語義學版本命名;
- b)若一個Module的版本為v2及以上,模塊的主版本必須包含在模塊路徑及引用路徑中(例如,聲明方:module github.com/my/mod/v2,引用方:require github.com/my/mod/v2 v2.0.0,包引用處:import "github.com/my/mod/v2/mypkg");
- c)例外,若模塊主版本為v0或v1,模塊路徑及引用路徑無須包含主版本。
通常來講,引用路徑不同的包是兩個全然不同的包(如math/rand和crypto/rand是兩個不同的包)。同樣,包含不同主版本的引用路徑所標識的包亦是兩個不同的包。因此,example.com/my/mod/mypkg與example.com/my/mod/v2/mypkg是不同的包,且可能會在一次構建中同時引用。
因有些模塊還未轉換為Module方式,過度期,會支持如下幾個例外:
- a)gopkg.in
- 會繼續支持gopkg.in/yaml.v1或gopkg.in/yaml.v2等引用方式。
- b)當引用還未Module化的v2+版本包時,會有'+incompatible'后綴。
- c)當Module模式未開啟時,采用最小模塊兼容性。
即在Go 1.11鄰近版本,不開啟Module模式時(GO111MODULE=off),引用v2或以上版本,不會將版本加入路徑中。
3 使用
3.1 模塊支持激活
安裝Go 1.11及以上版本,然后可以使用如下兩種方式中的任一種激活模塊支持。
- a)在$GOPATH/src文件夾之外使用go命令,且當前文件夾或其上層文件夾包含go.mod文件,而GO111MODULE環境變量未設置或設置為了auto;
- b)設置GO111MODULE=on,然后調用go命令。
即在$GOPATH/src之外使用模塊支持,無需設置GO111MODULE環境變量,而在$GOPATH/src使用模塊支持,需將GO111MODULE設置為on。
3.2 定義一個模塊
a)進入對應文件夾
$ cd path
該文件夾可以為設置GO111MODULE=on的$GOPATH/src,或該文件夾之外的任意路徑。
b)執行go mod init
$ go mod init github.com/my/repo
若在初始化一個v2+的模塊,需要手動更改go.mod文件及.go代碼,以在引用路徑及模塊路徑加入版本信息(語義學版本引用)。
c)構建模塊
$ go build ./...
“./...”模式匹配了當前模塊下的所有包,go build將自動增加缺失的包。
d)測試模塊
$ go test ./...
或者執行如下語句,可以運行模塊內的測試及所有直接及間接依賴測試以檢查不兼容問題。
$ go test all
3.3 依賴升降級
可以使用go get命令進行日常依賴升級及降級,其會自動更新go.mod文件,當然您也可以手動編輯go.mod文件。
當然,go get也如go build,go test一樣,會自動加入缺失的依賴包。
查看可用的小版本或補丁更新,可以執行:
$ go list -u -m all
將直接或間接依賴更新為最新的小版本或補丁版本,可以執行:
$ go get -u
僅更新為補丁版本,可以執行:
$ go get -u=patch
go get foo等同于go get foo@latest,會將foo更新為最新版本。
當有語義學版本時,最新版本為語義學最新版本,沒有時,為最新的提交。
一個通常錯誤的認為是,go get -u foo僅獲取最新版本的foo。其實其還會獲取foo的直接或間接依賴的最新版本。
更新版本,推薦的做法是先運行go get foo,好使時再運行go get -u foo。
進行版本升降級時,可以使用@version后綴,如:
$ go get [email protected]
或
$ go get foo@e3702bed2
還支持模塊查詢,如:
$ go get foo@'<v1.6.2'
使用分支名稱,可以不考慮其是否有語義學版本,而更新為最新的分支提交。
$ go get foo@master
版本升降級后,需測試是否有不兼容問題:
$ go test all
3.4 模塊版本發布
發布前執行如下命令,以刪減未使用的包。
go mod tidy
然后執行如下命令,保證兼容性。
go test all
然后發布時,需將go.sum文件與go.mod一起提交。
發布v2及以上版本時需注意滿足語義學版本引用規則,版本需包含在模塊路徑及引用路徑中。創建一個v2及以上的版本,有如下兩種方式:
a)不創建子文件夾
go.mod文件包含vN路徑(如:module github.com/my/module/v3),模塊內的包引用亦需修改為包含版本的格式(如:import "github.com/my/module/v3/mypkg")。
b)創建子文件夾
創建vN子文件夾,且將go.mod放至該文件夾下,模塊路徑需以/vN結尾,然后將代碼拷貝至vN子文件夾下,然后更新模塊內的包引用路徑(如:import "github.com/my/module/v3/mypkg")。
最后,創建一個滿足語義學版本的tag,推送至倉庫即可。
但需注意子模塊的情形,該種情形tag需包含前綴。
如,我們有模塊example.com/repo/sub/v2,然后想發布版本v2.1.6,倉庫為example.com/repo,子模塊定義在sub/v2/go.mod,提交時tag需命名為sub/v2.1.6。
參考資料
[1] https://github.com/golang/go/wiki/Modules
[2] https://research.swtch.com/vgo
原文鏈接:https://leileiluoluo.com/posts/golang-modules.html
本文作者:磊磊落落的博客