介紹
本文是系列博客的第一部分盗飒。
- Part 1 —— 使用 Go Modules (本文)
- Part 2 —— 遷移到 Go Modules
- Part 3 —— 發(fā)布 Go Modules
- Part 4 —— Go Modules 的版本升級
- Part 5 —— Go Modules 兼容
Go 1.11 版本初步添加了對 modules 特性的支持拓哟,作為 go 語言新的依賴管理系統(tǒng)斑举,可以幫助我們更明確、容易的管理依賴包的版本信息渤愁。在這篇文章中缰揪,我們將會介紹入門 modules 特性所需掌握的一些基本操作眨业。
module 是一系列包的集合竹握,以文件樹的形式記錄在go.mod文件中画株,該文件位于項目根路徑。go.mod文件主要定義了module的路徑(也就是使用該module的import路徑值)啦辐,以及該module本身對其他module的依賴信息谓传。其中每一個依賴項module都包含該module的導(dǎo)入路徑及專門的版本號,版本號要符合MAJOR.MINOR.PATCH
格式昧甘。
從 Go 1.11 開始良拼,如果當前工作路徑或其父路徑下存在go.mod文件战得,且不在$GOPATH/src
目錄內(nèi)的話充边,go 命令會默認使能modules特性,反之常侦,如果在$GOPATH/src
目錄內(nèi)的話浇冰,為了兼容起見,go命令還會以傳統(tǒng)的GOPATH模式運作聋亡,即便有g(shù)o.mod文件存在肘习。但從 Go 1.13 開始,module模式將會成為默認模式坡倔。即不管當前路徑是否位于$GOPATH/src
路徑下漂佩,只要含go.mod文件則默認開啟module模式,除非你修改了環(huán)境變量GO111MODULE
值罪塔,將其置為了off 投蝉。
本文將會從以下幾個步驟來介紹使用 modules 特性開發(fā)過程中所涉及的幾個通用操作:
- 創(chuàng)建一個新的 module
- 為該 module 添加一個依賴
- 升級依賴
- 升級 module 大版本后,添加一個新的依賴
- 升級 module 大版本后征堪,更新一個已有的依賴
- 移除冗余的依賴項
創(chuàng)建一個新的 module
現(xiàn)在我們開始創(chuàng)建一個新的 module 瘩缆。
在$GOPATH/src
目錄外部某個路徑,新建一個空的文件夾佃蚜,并創(chuàng)建一個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)
}
}
此時,這個目錄下包含一個包——hello谐算,但因為沒有g(shù)o.mod文件熟尉,所以還算不上是一個module,這時候我們運行go test
命令可以看到:
C:\Users\Lenovo\IdeaProjects\studyGoModules>go test
PASS
ok _/C_/Users/Lenovo/IdeaProjects/studyGoModules 0.240s
C:\Users\Lenovo\IdeaProjects\studyGoModules>
很奇怪洲脂,我們既沒有在$GOPATH/src
目錄下工作斤儿,同時當前目錄也沒有g(shù)o.mod文件,按理說hello_test.go會報找不到Hello()方法的錯誤,但為啥跑過了呢雇毫?原來是go命令找不到任何路徑玄捕,于是基于其當前工作路徑名編造了一個假的的導(dǎo)入path:_/C_/Users/Lenovo/IdeaProjects/studyGoModules,通過這種方法棚放,他找到了hello.go文件中定義的Hello()方法枚粘,使得測試跑過了。
接下來飘蚯,我們通過命令go mod init
來創(chuàng)建一個根路徑為當前路徑的module并且再次執(zhí)行go test
查看效果:
C:\Users\Lenovo\IdeaProjects\studyGoModules>go mod init gitee.com/atix/hello
go: creating new go.mod: module gitee.com/atix/hello
C:\Users\Lenovo\IdeaProjects\studyGoModules>go test
PASS
ok gitee.com/atix/hello 0.244s
C:\Users\Lenovo\IdeaProjects\studyGoModules>
可以看到我們創(chuàng)建了一個名為```gitee.com/atix/hello``的module馍迄,并且測試了這個module的可用性。同時還在當前路徑創(chuàng)建了一個go.mod文件:
module gitee.com/atix/hello
go 1.14
go.mod文件只會出現(xiàn)在一個module的根路徑下局骤。我們可以在某個module目錄中新建子目錄并創(chuàng)建新的package攀圈,只需在引用該package時,import路徑包含上module的import路徑即可峦甩。如:我們上面創(chuàng)建了一個新的module:gitee.com/atix/hello赘来,我們在根目錄下新建文件夾:subHello,并新建文件subHello.go:
package subHello
import "fmt"
func SubHello() string {
return "SubHello, world!"
}
這樣凯傲,當我們需要在別的地方使用SubHello()方法時(當然犬辰,首先我們得發(fā)布hello module),import路徑要加上hello module的路徑冰单,即:
import "gitee.com/atix/hello/subHello"
為該 module 添加一個依賴
Go Modules 的主要目的是更好的使用其他開發(fā)者開發(fā)的代碼幌缝。
接下來我們嘗試在hello module中使用其他開發(fā)者發(fā)布的module,首先我們改寫一下hello.go:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
然后我們再次運行go test
命令看下效果(因本人網(wǎng)絡(luò)問題訪問https://proxy.golang.org/rsc.io/quote/@v/list 異常诫欠,故此處貼原文執(zhí)行效果):
$ 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 gitee.com/atix/hello 0.023s
$
這里我找到了rsc.io/quote的go.mod文件:
module "rsc.io/quote"
require "rsc.io/sampler" v1.3.0
以及rsc.io/sampler的go.mod文件:
module "rsc.io/sampler"
require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c
可以看到涵卵,從hello到quote,再到sampler荒叼,依次依賴了下面3個module:
- rsc.io/quote v1.5.2
- rsc.io/sampler v1.3.0
- golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
同時我們可以在go test
的執(zhí)行日志中看到這3個module的find->downloading->extract操作轿偎,即:go 命令會分析go.mod文件,并遞歸式的處理require模塊所聲明的依賴項module甩挫,處理內(nèi)容包括下載及解壓贴硫,解壓后的依賴項module內(nèi)容會緩存在$GOPATH/pkg/mod
路徑中。
go test
執(zhí)行的效果還包括:將解析后的依賴項module信息添加到本module的go.mod文件中伊者,注意英遭,默認只添加直接依賴的module。關(guān)于依賴項module的版本號亦渗,默認使用其最新版本挖诸。體現(xiàn)為:
$ cat go.mod
module gitee.com/atix/hello
go 1.14
require rsc.io/quote v1.5.2
$
值得注意的是,雖然 go 命令使得添加依賴變得超級方便法精,但這是有代價的多律。你會發(fā)現(xiàn)自己的模塊在某些關(guān)鍵領(lǐng)域痴突,比如說正確性、安全性以及許可性等方面會相當依賴新添加的依賴項modules狼荞。這方面想了解更多的話辽装,可以參閱博客Our Software Dependency Problem。
正如上面我們看到的那樣相味,添加一個直接依賴的同時也會帶來一些間接依賴拾积。執(zhí)行命令go list -m all
可以查看當前module的所有依賴項信息:
$ go list -m all
gitee.com/atix/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$
注意此命令會遞歸式分析go.mod文件,如果你跟我一樣因網(wǎng)絡(luò)原因執(zhí)行
go test
失敗導(dǎo)致go.mod文件沒更新的話丰涉,此命令只會顯示本module(別名:主 module )信息拓巧,即上面的第一行:gitee.com/atix/hello 。
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c :此依賴項版本號被稱為"Pseudo-versions"式版本號一死,代表該module代碼的某次提交版本肛度,該提交尚未打tag,由3部分組成投慈,代碼提交前最近的一次版本號承耿,代碼提交時間的UTC格式,以及代碼提交號的hash前綴逛裤。更多詳情戳此
除了go.mod文件外瘩绒,go命令還維護了一個名為go.sum的文件猴抹,該文件主要包含各依賴項module版本內(nèi)容的加密哈希值带族。如果未依賴其他module則不會生成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.sum文件的幫助蟀给,go命令可以確保將來下載這些module的代碼時能夠獲取跟第一次下載時相同的內(nèi)容蝙砌,以保證你的項目依賴的這些module不會發(fā)生意想不到的改變,不管是惡意的跋理,偶然的還是意外的择克。故,請將go.mod前普、go.sum文件都納入版本管理肚邢。
升級依賴
使用 Go Modules 功能過程中,版本號要符合一定的語法格式拭卿。一個合法格式包括3部分:主版本骡湖、次版本、補丁版本峻厚。舉個例子响蕴,拿v0.1.2
來說,主版本號是0惠桃,次版本號是1浦夷,補丁版本是2辖试。接下來,我們一起來對gitee.com/atix/hello
進行一次次版本號升級劈狐。
通過分析命令go list -m all
的輸出結(jié)果罐孝,我們可以看到golang.org/x/text
模塊的版本是一個未打tag的版本號,接下來我們把 golang.org/x/text
升級到最新的一個tag版本并測試一下項目是否依然有效:
$ 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
$
關(guān)于命令
go get
肥缔,完整的命令肾档,在module名后面需要加上@version字段來指明要獲取該module哪個版本,默認情況下會獲取其最新版本
很棒辫继,項目運行正常怒见,接下來讓我再看一眼項目的依賴情況是否發(fā)生了變化:
$ 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 gitee.com/atix/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
)
$
注釋
// indirect
表示這一行的依賴并不是本module直接依賴使用的,而是其他module依賴使用的姑宽。想了解更多的話可以執(zhí)行命令go help modules
來查看細節(jié)遣耍。
可以看到golang.org/x/text
已經(jīng)升級到了最新的版本,同時go.mod文件也更新了炮车,增加了golang.org/x/text
模塊的描述信息舵变,前面我們提到過,默認情況下執(zhí)行go test
時瘦穆,go.mod文件只會加載直接依賴的module信息纪隙,為啥這次把golang.org/x/text
這個間接依賴的module信息加上了呢,這是因為我們在本地執(zhí)行了go get golang.org/x/text
命令后扛或,本地開發(fā)調(diào)試用到該module的代碼時都會用本地的最新版本golang.org/x/text
代碼绵咱,而不是之前那個未打tag的版本,這種情況下熙兔,如果我們發(fā)布了自己的module悲伶,別人引用時,才能正常使用住涉。
需要注意的是麸锉,通過本地go get
方式直接覆蓋原依賴module版本時會出現(xiàn)不兼容問題,換句話說舆声,本來以來的那個module版本是v1.1.0花沉,但你通過go get
方式獲取的是v1.2.0,且該module的v1.2.0未兼容v1.1.0媳握,那么使用時就會出錯碱屁。因此,如果想要本地升級某個依賴module的版本毙芜,最好在get的時候指定某個具體的版本忽媒,如:go get rsc.io/sampler@v1.3.1
命令
go list -m -versions ${module_name}
可以列出${module_name}
所有可用版本
添加一個新的依賴,該依賴是某個已有依賴module的大版本升級中引入的特性
截止目前為止腋粥,我們的gitee.com/atix/hello模塊直接依賴的module只有一個rsc.io/quote v1.5.2晦雨,接下來我們將要引入一個新的module:rsc.io/quote/v3@latest架曹,并在我們代碼中調(diào)用其Concurrency()方法,修改hello.go文件如下:
package hello
import (
"rsc.io/quote"
v3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func V3Hello() string {
return v3.Concurrency()
}
同時在測試代碼中添加對新方法V3Hello()的測試方法:
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)
}
}
func TestV3Hello(t *testing.T) {
want := "Concurrency is not parallelism."
if got := V3Hello(); got != want {
t.Errorf("V3Hello() = %s, want %s", got, want)
}
}
接下來執(zhí)行go test
查看效果:
$ 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 gitee.com/atix/hello 0.024s
$
此時闹瞧,如果我們查看go.mod文件時绑雄,會發(fā)現(xiàn)require塊同時包含rsc.io/quote v1.5.2和rsc.io/quote/v3 v3.1.0兩行,那么這個要怎么理解呢奥邮?
回顧我們本段標題万牺,什么叫升級大版本?對于每一個module來說洽腺,主版本升級意味著不同的module path脚粟,從v2開始,每升級一個大版本蘸朋,該module的import路徑都要加一層核无。拿本例中的rsc.io/quote來說,第一次我們import時寫的是import rsc.io/quote
藕坯,那么當go命令在解析依賴關(guān)系時团南,就回去拉rsc.io/quote第一個大版本的最新版,即v1.3.2炼彪,后來我們新import了import rsc.io/quote/v3
吐根,那么當go命令在解析依賴關(guān)系時,就回去拉rsc.io/quote第三個大版本的最新版辐马,即v3.1.0拷橘。
這是go語言對module版本的語法定義,默認情況下齐疙,同一個大版本內(nèi)的小版本之間膜楷,應(yīng)該是向下兼容的。這樣如果我們之前依賴了某個module的低版本贞奋,當后期需要升級其次版本號以使用某個新增的方法或者特性時,就不需要修改import的path穷绵,只需要執(zhí)行go get XXX@YYY
來獲取其新的版本即可轿塔。
對于同一個module path,在同一次build中仲墨,go命令最多只允許include一次勾缭。通過變更module path的方式來升級大版本號可以允許我們在一次build同時include該module的多個版本,因為他們的module path不同目养。這在某些時候尤其有用俩由,想象這樣一種場景,作為使用方癌蚁,我們在項目中依賴了某個module A幻梯,某天A升級了大版本兜畸,添加了某個有用的特性,但新版本是否兼容了舊版本尚未可知碘梢,此時我們既想用新特性咬摇,又沒有那么多時間去驗證之前使用部分的兼容性,怎么辦煞躬,沒關(guān)系肛鹏,只要A遵循了大版本升級修改module path的原則,那么我們可以在項目中同時使用A新舊兩個版本恩沛,只需要在import時對新版本加個別名即可在扰,如前面我們的做法:import v3 rsc.io/quote/v3 就是給quote的大版本3起了個別名v3。
將已有的某個依賴module升級到其新的大版本
上一節(jié)中雷客,我們同時引入了rsc.io/quote的v1和v3兩個版本健田,雖然在當時是方便了,但現(xiàn)在我們想要簡化依賴樹佛纫,僅使用rsc.io/quote的最新版本妓局。這時候我們要考慮至少以下幾個方面,大版本升級后:
- 低版本中的接口是否被移除
- 是否命名變了
- 是否出入?yún)⒆兞?/li>
此時最直接的方法通讀rsc.io/quote/v3的更新文檔來了解呈宇,查閱某個module的文檔可以通過執(zhí)行go doc
命令實現(xiàn):
$ 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
$
可以看到rsc.io/quote/v3
中已不再有Hello()方法好爬,新的方法名為HelloV3(),此時我們修改我們項目中對Hello()方法的使用甥啄,如下:
package hello
import v3 "rsc.io/quote/v3"
func Hello() string {
return v3.HelloV3()
}
func V3Hello() string {
return v3.Concurrency()
}
注意對比兩個版本的hello.go存炮,變化有兩個地方:
- import處移除了對rsc.io/quote的引用
- 方法Hello()中有原先的
return quote.Hello()
改為了return v3.HelloV3()
,包名和方法名都做了變更
至此蜈漓,我們項目中對rsc.io/quote的依賴就變成單一的rsc.io/quote/v3了穆桂,就不再需要在import對其進行別名處理了:
package hello
import "rsc.io/quote/v3"
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
這里有個小插曲,樓主這邊是用的是IDEA融虽,該工具在判斷import包的使用情況時比較死板享完,只認最后一層路徑,他發(fā)現(xiàn)你沒有使用v3有额,就會把import那行給刪掉般又,坑爹
移除未使用的依賴
雖然上面我們刪掉了rsc.io/quote
的導(dǎo)入,但當我們執(zhí)行go list -m all
或者查看go.mod文件時巍佑,發(fā)現(xiàn)其仍然存在:
$ go list -m all
gitee.com/atix/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 gitee.com/atix/hello
go 1.14
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構(gòu)建某個獨立的包時,go命令知會檢查是否有包丟失了萤衰,或者需要添加進來堕义,但卻不會移除冗余的包。想要移除冗余的module信息可以執(zhí)行命令go mod tidy
來實現(xiàn):
$ go mod tidy
$ go list -m all
gitee.com/atix/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 gitee.com/atix/hello
go 1.14
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 gitee.com/atix/hello 0.020s
$
總結(jié)
Go Modules 是未來 Go 語言的依賴管理體系脆栋,且此特性在 Go 1.11 之后均支持倦卖。
圍繞go.mod文件洒擦,本文介紹了使用 Go Modules 時涉及的幾個命令:
-
go mod init
創(chuàng)建一個新的module,并初始化go.mod文件 -
go build, go test
將新增的依賴module信息加載到go.mod文件中 -
go list -m all
解析go.mod文件并打印當前module的依賴信息 -
go get
可以用來修改或者新增某個依賴module信息 -
go mod tidy
可以移除冗余的module信息
在實際開發(fā)過程中糖耸,我們提倡大家使用modules特性秘遏。