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