使用GoModule(Using Go Modules譯文)

使用GoModule

英文原版:https://blog.golang.org/using-go-modules

介紹

這是系列文章的第一部分

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 pathmodule 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/samplergolang.org/x/text,只有直接依賴會記錄到go.mod:

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

go.modup-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-14c0d48ead0cpseudo-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.modgo.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 getgo 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/quotersc.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/quotev1.6.0v1.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.2rsc.io/quote v1.6.0,但是允許依賴多個不同的major版本,因爲他們的導包路徑不同。這使得module的使用者可以增量升級到新的major版本。本例中既需要rsc/quote/v3 v3.1.0中的quote.Concurrency又需要繼續使用rsc.io/quote v1.5.2,在比較大的項目中這種增量升級的能力特別重要。

升級依賴到新主版本

現在來完成從rsc.io/quotersc.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 buildgo 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.modgo.sum

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章