使用GoModule
英文原版:https://blog.golang.org/using-go-modules
介紹
這是系列文章的第一部分
- 1-使用GoModule(當前)
- 2-遷移到GoModule
- 3-發佈GoModule
- 4-GoModule: v2及新版本
Go1.11和Go1.12包含了初步的GoModule支持,這是一種新的依賴管理系統,它能夠更簡單精確地管理依賴包的版本信息。本文將介紹使用GoModule所需要的基本操作。
一個module是一個存儲在go.mod
文件中的GoPackage的集合,go.mod
文件定義了這個module的module path(此module被import的根路徑)及其所需依賴(其他被此module依賴的module)。每個依賴都以semantic version
的形式作爲一條module path
寫入go.mod
。
在Go1.11時,在$GOPATH/src
之外的文件夾或任意父級路徑包含go.mod
文件時,可以在go命令中使用GoModule。(考慮到兼容性,$GOPATH/src
下的文件夾即使存在go.mod
文件,也以GOPATH模式運行。)從Go1.13開始module模式會成爲默認開發模式。
本文主要內容如下:
- 新建module
- 添加依賴
- 升級依賴
- 添加新major版本的依賴
- 升級依賴到新major版本
- 移除未使用的依賴
新建module
在$GOPATH/src
之外新建一個文件夾,並在文件夾中創建hello.go
:
package hello
func Hello() string {
return "Hello, world."
}
然後再寫個hello_test.go
:
package hello
import "testing"
func TestHello(t *testing.T) {
wang := "Hello, world."
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
現在這個package還不是module,因爲沒有go.mod
文件,假設當前是在/home/gopher/hello
下,運行go test
會看到:
$ go test
PASS
ok _/home/gopher/hello 0.020s
$
最後一行是全部package的test總結。由於當前既不是在$GOPATH
下也不是module模式,而且go命令發現沒有用到import,因此當前路徑就成了一個“fake”的GOPATH。
現在我們執行go mod init
來啓用module模式並把當前路徑做成module的根路徑,然後再執行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
$
現在我們就完成了新建module,go mod init
命令生成了一個go.mod
文件:
$ cat go.mod
module example.com/hello
go 1.12
$
go.mod
文件只會出現在module的根目錄,子目錄的import path
由module path
及子目錄的相對路徑組成。假設存在子目錄world
,那麼這裏就不需要執行go mod init
了,這個package會被自動認爲是example.com/hello
的一部分,其導包路徑爲example.com/hello/world
。
添加依賴
GoModule的主要目的就是提高使用他人代碼(即添加依賴)的體驗。現在在上面的例子中添加一個import:
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
中沒有的import,則自動把它的最新版本放進去。(最新版本指的是最新的穩定版或者最新的tag或commit。)本例中go test
解析到新的import——rsc.io/quote
的最新版本爲v1.5.2
,以及它用到的兩個依賴——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.mod
up-to-date的情況下再次執行go test
將不會重複此操作,下載過的module會被緩存到$GOPATH/pkg/mod
中。
注意雖然go命令方便快捷,但不意味到此結束。現在module嚴格依賴go.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
$
可以看出當前module作爲main module
總是在第一行出現,然後是按module的path排序的依賴包。
依賴包golang.org/x/text
的版本v0.0.0-20170915032832-14c0d48ead0c
是pseudo-version
的一種,這也是對於沒有tag情況下的版本語法。
另外go.sum
會作爲go.mod
的附屬,包含各個module版本的cryptographic hashes
:
$ 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
來確保將來下載的module跟首次下載有相同的校驗和,依次確保項目所以來的module未被篡改。go.mod
與go.sum
都應該被包含進版本控制。
升級依賴
在GoModule中,版本通過semantic版本標籤來提現。semantic版本標籤包含三個部分:major、minor、patch,比如v0.1.2的major是0,minor是1,patch是2。下面看下minor版本升級,下一部分看major版本升級。
從go lit -m all
的輸出中可以看到,當前使用的golang.gor/x/text
是沒有tag的版本,現在更新到最新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
$
一切正常,然後看下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
已經升級到了最新tag版本(v0.3.0),並且go.mod
也同步更新。這裏的indirect
註釋表示這個依賴並沒有被當前module直接使用,只被其他依賴所依賴。可以通過go help modules
查看詳細信息。
現在將rsc.io/sampler
升級一下minor版本,同樣執行go get
及go test
:
$ 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
並不兼容當前使用,來看下這個依賴包的可用tag版本列表:
$ 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/[email protected]
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:func Proverb
來返回一個go併發諺語,通過調用rsc.io/quote/v3
中的quote.Concurrency
來實現。首先在hello.go
中添加func:
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 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
兩個module:
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$
一個module的每個不同的major版本都有不同的導包路徑:從v2開始,導包路徑必須以major版本結尾。例如v3
版本的rsc.io/quote
導包路徑就是rsc.io/quote/v3
。這種習慣稱爲semantic import versioning
,同時它使不同major版本間的不兼容依賴包具有不同的名字。因此,rsc.io/quote
的v1.6.0
與v1.5.2
兩個版本複用了rsc.io/quote
。(前面的例子中,rsc.io/sampler v1.99.99
應該是向後兼容rsc.io/sampler v1.3.0
的,但是bug或其他錯誤可能會導致這種情況。)
go命令允許在一次構建中每個獨立的導包路徑包含最多一個版本,也就是說最多存在每個major版本中的一個版本:一個rsc.io/quote
,一個rsc.io/quote/v2
,一個rsc.io/quote/v3
等。這相當於告訴module的作者一個明確的規則:對於一個module來說,不允許同時依賴rsc.io/quote v1.5.2
與rsc.io/quote v1.6.0
,但是允許依賴多個不同的major版本,因爲他們的導包路徑不同。這使得module的使用者可以增量升級到新的major版本。本例中既需要rsc/quote/v3 v3.1.0
中的quote.Concurrency
又需要繼續使用rsc.io/quote v1.5.2
,在比較大的項目中這種增量升級的能力特別重要。
升級依賴到新主版本
現在來完成從rsc.io/quote
到rsc.io/quote/v3
的升級,由於major版本變了,我們需要預料到有些API可能有不兼容變更,看過依賴包的doc之後,可以看到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
$
(這裏有個已知的bug,所顯示的import中丟失了v3
)
現在將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()
}
到這裏就沒必要重命名import了:
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.mod
中,並且執行go list -m all
也能看到:
$ 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
這樣構建單個包時可以很容易判斷缺了什麼需要加什麼,但是不能確定什麼可以被安全移除。僅當檢查完module中的所有包並且這些包都整合之後才能移除未使用的依賴。普通的構建命令不會加載此信息,因此它不能安全地刪除依賴。
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
$
總結
GoModule是go依賴管理的未來,moduel模式在所有已支持的go版本(go1.11與go1.12)都可以使用。
本文介紹了使用GoModule的以下工作流:
go mod init
創建新的module,同時初始化go.mod
build``go test
及其他構建命令會按需將依賴添加到go.mod
go list -m all
查看當前module的依賴go get
變更依賴的指定版本(或者添加新的依賴)go mod tidy
移除未使用的依賴
我們鼓勵開發者在開發中開始使用GoModule並在項目中添加go.mod
及go.sum
。