Go 的包管理一直被人詬病,有人提出過解決方法,好比 godep、govendor 等工具,但在 G1.11 版本中,Go 官方很霸道的提出了 Go Module 方案,雖然被人吐槽,但如今已經成爲事實上的包管理方案。git
Go 官方也經過一系列的博客來介紹 Go Modules,這是系列的第一篇文章。github
這個系列的文章總共有 5 篇,這是第一篇:golang
使用 Go Modulesc#
遷移到 Go Modules緩存
發佈 Go Modules安全
Go Modules:V2 及後續版本併發
保持 Modules 的兼容性ide
Go1.11 和 1.12 版本中初步支持了 Modules,Go 的新依賴管理系統使得依賴的版本信息更加清晰以及更容易管理。這篇文章將介紹 Go Modules 的基本使用。工具
一個 Module 是一系列 Go 的包組成的文件樹,而且在根目錄下有一個 go.mod 文件。go.mod 文件中定義了Module 的路徑(module path),這也是根目錄的包路徑(import path),以及依賴需求,這是構建應用所須要的其餘 Modules。每一個依賴都有一個 module path 和語義版本號。測試
從 Go1.11 開始,只要當前目錄或者任何父目錄中有 go.mod 文件,就可使用 module 相關的命令,但這個目錄必需要在GOPATH/src以外。(在GOPATH/src 目錄下,爲了保持兼容性,即便 go.mod 文件存在,也只能運行老版本的命令。從命令文檔中查看更多細節),從 Go1.13 開始,module 將做爲默認的開發模式。
這篇文章介紹了使用 Go modules 來開發時的一系列常見操做:
建立 module
添加依賴
更新依賴
爲依賴添加主版本號
爲依賴更新主版本號
移除無用的依賴
首先來建立一個新的 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) { want := "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 $複製代碼
最後一行展現了測試的狀況。由於當前不在 $GOPATH 下,也不在任何模塊下,go 命令知道當前目錄沒有包路徑(Import path),就基於當前的目錄名稱建立了一個假的包路徑:_/home/gopher/hello。
接下來在當前的目錄中使用 go mod init
來建立一個 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 的根目錄。子目錄中包的包路徑具備由 module 路徑和子目錄路徑組成。好比如今建立了一個子目錄 world,不須要在目錄中再次運行 go mod init。這個包會被自動設別爲 example.com/hello
module 的一部分,包路徑是 example.com/hello/world。
Go modules 被創造的最主要的動機是改善使用其餘開發人員的代碼的體驗(即增長依賴)。
下面在 hello.go 中導入 rsc.io/quote 並用它來實現 Hello 方法:
package hello import "rsc.io/quote" func Hello() string { return quote.Hello() }複製代碼
如今再次運行測試:
$ 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 中定義時,go 命令會自動將這些依賴的最新版本添加到 go.mod 文件中。(Latest 表示最新標記的穩定(非預發佈)版本,或者最新標記的預發佈版本,或者最新的未標記版本。)在這個例子中,go test 會把 rcs.io/quote 解析爲 rcs.io/quote 模塊,版本爲 v1.5.2。同時也會下載 rsc.io/quote 的兩個依賴: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 test 的不會再次下載依賴,由於 go.mod 如今是最新狀態,下載的依賴被緩存在本地(在 $GOPATH/pkg/mod)。
$ go test PASS ok example.com/hello 0.020s $複製代碼
須要注意的是,雖然 go 命令能夠快速、容易的添加依賴,但這是有代價的。你的模塊功能的正確性、安全性、和許可證被你引入的那些新依賴決定(Ray注:意思是引入的這些依賴的安全性、程序是否被測試、是否引入了侵權的代碼,這些都無法保證),而這只是其中的一小部分問題。更深刻的思考,請查看 Russ Cox 的博客,咱們的軟件依賴問題。
在前面能夠看到,直接引入的依賴一般也會引入一些間接的依賴。 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 $複製代碼
在 go list 的輸出中,當前模塊,也被稱之爲主模塊會出如今第一行,下面跟着依賴的模塊路徑。
golang.org/x/text 的版本號 v0.0.0-20170915032832-14c0d48ead0c 叫作 Pseudo-versions,這是go 命令用於未打標記的提交的版本語法。
另外對於 go.mod,go 命令會維護一個 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 命令使用 go.sum 文件確保後續下載這些模塊所得到的內容與第一次下載得到的內容相同,確保項目所依賴的模塊不會被意外更改,不管是惡意的、偶然的或者其餘緣由。go.mod 和 go.sum 都應該被歸入版本管理。
在 Go 的模塊中,版本號使用語義版原本表示。一個語義版本有三個部分:主版本號、次版本號、補丁版本號。好比 v0.1.2,主版本是 0, 次版本是 1,補丁版本號 2。讓咱們來看一下次版本的更新,下一節,將會介紹主版本號的更新。
從 go list -m all 的輸出中,咱們看到了 golang.org/x/text 使用了未標記的版本號。咱們把它更新到最新的標記版本,更新以後,上面的代碼也經過了測試:
$ 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 已經被更新到最新的標記版本(v0.3.0)。go.mod 文件中也被更新到了 0.3.0 版本。 indirect
表示這個依賴不是直接被模塊使用,只是間接的被其餘的模塊依賴。經過 go help modules
能夠查看更多的細節。
如今,讓我試着用一樣的方法來更新 rsc.io/sample 的次版本號,先執行 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 與程序不兼容。來看一下,這個模塊全部可用的標記版本:
$ 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/sampler@v1.3.1 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,表示以前定義的最新版本。
讓咱們在包中添加一個新的方法, Proverb
方法返回一個 Go 的併發諺語,經過 rsc.io/quote/v3 中的quote.Concurrency
方法來提供。首先在 hello.go 中添加一個新方法:
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: 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 這兩個依賴同時存在:
$ go list -m rsc.io/q... rsc.io/quote v1.5.2 rsc.io/quote/v3 v3.1.0 $複製代碼
Go 模塊每一個不一樣的主版本號(v1,v2 等等)都使用不一樣的模塊路徑,從 v2 開始,路徑必須以主版本號結尾。在這個例子中,rsc.io/quote 的 v3 版本的路徑再也不是 rsc.io/quote,而是 rsc.io/quote/v3。這種習慣被稱之爲語義導入版本,會給不兼容的包(擁有不一樣的主版本號)不一樣的名稱。相反 rsc.io/quote 的 v1.6.0 必須向後兼容 v1.5.2,因此會重用 rsc.io/quote 這個路徑名稱。(在以前的版本中,rsc.io/sampler v1.99.99 應該向後兼容 rsc.io/sample v1.3.0,可是由於 bug 或者不正確的客戶端存在,模塊的這些行爲都是有可能發生的。)
go 命令在構建中只容許任何特定的模塊存在至多一個主版本,意味着只能每一個模塊的主版本只能出現一次:一個 rsc.io/quote,一個 rsc.io/qutoe/v2,一個 rsc.io/quote/v3,以此類推。這爲模塊做者提供了關於單個模塊路徑可能重複的明確規則:rsc.io/quote v1.5.2 和 rsc.io/quote v1.6.0 不能同時出如今同一次構建中。同時,容許有不一樣主版本的模塊出現類同一個構建中(由於擁有不一樣的路徑),這也給模塊的消費者能夠有增量升級主版本的能力。在這個例子中,咱們想要調用rsc/quote/v3 v3.1.0 中的 quote.Concurrency 方法,可是這個方法尚未在 rsc.io/quote v1.5.2 中實現。這種增量遷移的能力在大型的程序或者代碼庫中很重要。
讓咱們來完成從 rsc.io/quote 到 rsc.io/quote/v3 的遷移。由於主版本好改變,咱們認爲一些 API 可能已經刪除、重名或者作了其餘不兼容的修改。閱讀文檔,咱們能夠看到 Hello 已經被升級爲 HelloV3:
$ 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 $複製代碼
在 hello.go 中,咱們能夠把 quote.Hello() 使用 V3.HelloV3() 來替代:
package hello import quoteV3 "rsc.io/quote/v3" func Hello() string { return quoteV3.HelloV3() } func Proverb() string { return quoteV3.Concurrency() }複製代碼
並且,也再也不須要重命名導入了,因此能夠撤銷這個重命名:
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,能夠清晰的發現那些依賴是缺失的,須要被添加,但卻不能明確哪些依賴能夠被安全刪除。只有檢查模塊中全部的包以及這些包全部的可能的構建組合以後,若是這個依賴仍是沒有被用到,才能刪除這個依賴。一般的構建命令不會去作這些檢查,也就不能安全的刪除依賴。
使用 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 $複製代碼
Go modules 是 Go 依賴管理的將來。模塊功能能夠在全部支持的 Go 版本中使用(包括 Go1.11 和 Go1.12)。
這篇文章中介紹了 Go modules 的這些功能:
go mod init 建立一個新的模塊,並初始化描述這個模塊的 go.mod 文件
go build,go test 和其餘包內構建命令添加須要的新依賴到 go.mod 文件中
go list -m all 打印當前模塊全部的依賴
go get 改變當前依賴的新版本(或者添加一個新的依賴)
go mod tidy 移除無用的依賴
咱們鼓勵你在本地的開發中在項目中添加 go.mod 和 go.sum,開始使用模塊功能。請向咱們發送bug 報告和體驗報告,幫助咱們改善 Go 的依賴管理功能。
感謝您全部的反饋和幫助改進模塊的建議。