每一個依賴管理解決方案都必須解決選擇依賴項版本的問題。當前存在的許多版本選擇算法都試圖識別任何依賴項的「最新最大(latest greatest)」版本。若是您認爲語義版本控制(sematic versioning)將被正確應用而且這種社會契約獲得遵照,那麼這是有道理的。在這樣的狀況下,依賴項的「最新最大」版本應該是最穩定和安全的版本,而且應與較早版本具備向後兼容性。至少在相同的主版本(major verion)依賴樹中是如此。git
Go決定採用其餘方法,Russ Cox花費了大量時間和精力撰寫文章和演講探討Go團隊的版本選擇方法,即最小版本選擇或MVS(Minimal Version Selection)。從本質上講,Go團隊相信MVS爲Go程序實現持久的和可重複的構建提供了最佳的方案。我建議你們閱讀這篇文章以瞭解Go團隊爲何相信這一點。github
在本文中,我將盡最大努力解釋MVS語義,展現一個實際的Go語言示例,並實際使用MVS算法。golang
將Go的依賴項版本選擇算法命名爲「最小版本選擇」是有點用詞不當,可是一旦您瞭解了它的工做原理,您會發現這個名稱真的很貼切。如我以前所述,許多選擇算法會選擇依賴項的「最新最大」版本。我喜歡將MVS視爲選擇「最新非最大(latest non-greatest)」版本的算法。並非說MVS不能選擇「最新最大」,而是隻要項目中的任何依賴項都不須要「最新最大」,那麼就不須要該版本。算法
爲了更好地理解這一點,讓咱們建立一種狀況,其中幾個module(A,B和C)依賴於同一module(D),可是每一個module都須要不一樣的版本。sql
上圖顯示了module A,B和C如何分別獨立地須要module D和各自須要D的不一樣版本。json
若是我啓動一個須要module A的項目,那麼爲了構建代碼,我還須要module D。module D可能有不少版本可供選擇。例如,假設module D表明sirupsen的logrus module。我能夠要求Go向我提供module D全部已存在(打tag)的版本列表。windows
清單1:api
$ go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0v1.4.0 v1.4.1 v1.4.2
清單2顯示了module D存在的全部版本,咱們看到其中顯示的「最新最大」版本爲1.4.2。安全
該項目應選擇哪一個版本的module D呢?確實有兩種選擇。首選是選擇「最新的」版本(在主要版本爲1的這一行中),即v1.4.2。第二個選擇是選擇module A所需的版本v1.0.6。併發
像dep這樣的依賴工具將選擇v1.4.2版,並在語義版本化和遵照社會契約的前提下能夠正常工做。可是,考慮到Russ Cox在這裏闡述的一些緣由,Go會尊重module A的要求並選擇版本1.0.6。在須要module的項目的全部依賴項的當前所需版本集合中,Go會選擇「最小」版本。換句話說,如今只有module A須要module D,而module A已指定它要求的版本爲v1.0.6,所需版本集合中只有v1.0.6,所以Go選擇的module D的版本便是它。
若是我引入要求項目導入module B的新代碼時會怎樣?將module B導入項目後,Go會將項目的module D版本從v1.0.6升級到v1.2.0。Go再次在項目依賴項module A和B的當前所需版本集合(v1.0.6和v1.2.0)中選擇了module D的「最小」版本。
若是我再次引入須要項目導入module C的新代碼時會怎樣?Go將從當前所需版本集合(v1.0.6,v1.2.0,v1.3.2)中選擇最新版本(v1.3.2)。請注意,版本v1.3.2仍然是module D(v1.4.2)的「最小」版本,而不是「最新最大」版本。
最後,若是刪除剛剛添加的依賴module C的代碼會怎樣?Go會將項目鎖定到module D的版本v1.3.2上。降級到版本v1.2.0將是一個更大的更改,而Go知道版本v1.3.2能夠正常並穩定運行,所以版本v1.3.2仍然是module D的「最新但非最大(latest non-greatest)「版本。另外,module文件(go.mod)僅維護快照,而不是日誌。沒有有關歷史撤消或降級的信息。
這就是爲何我喜歡將MVS視爲選擇「最新非最大(latest non-greatest)」module 版本的算法的緣由。但願您如今能夠理解爲何Russ Cox在命名算法時選擇名稱「minimal」。
有了上述基礎,我將用一個示例項目讓你看到Go和MVS算法實際是如何工做的。在此項目中,module D將用logrus module表明,而該項目將直接依賴於rethinkdb-go(moduleA)和golib(moduleB)module。rethinkdb-go和golib module直接依賴logrus module,而且每一個module都須要一個不一樣的logrus版本,而且這些版本都不是logrus的「最新」版本。
上圖顯示了三個module之間的獨立關係。首先,我將建立項目,初始化module,而後加載VS Code。
清單2:
$ cd $HOME$ mkdir app$ mkdir app/cmd$ mkdir app/cmd/db$ touch app/cmd/db/main.go$ cd app$ go mod init app$ code .
清單2顯示了全部要運行的命令。運行這些命令後,如下代碼應出如今VS Code中。
上圖顯示了項目結構和module文件應包含的內容。有了這個,如今該添加使用rethinkdb-go module的代碼了。
清單3:https://play.golang.org/p/bc5I0Afxhvc
01 package main0203 import (04 "context"05 "log"0607 db "gopkg.in/rethinkdb/rethinkdb-go.v5"08 )0910 func main() {11 c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)12 if err != nil {13 log.Fatalln(err)14 }1516 if _, err = c.Query(context.Background(), db.Query{}); err != nil {17 log.Fatalln(err)18 }19 }
清單3引入了rethinkdb-go module的major版本v5。添加並保存此代碼後,Go會查找、下載和提取module,並更新go.mod和go.sum文件。
清單4:
01 module app0203 go 1.130405 require gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
清單4顯示了go.mod須要rethinkdb-go module做爲直接依賴項,並選擇了v5.0.1版本,該版本是該module的「最新最大版本」。
清單5:
...github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=...
清單5顯示了go.sum文件中引入logrus module v1.0.6版本的兩行。在這一點上,您能夠看到MVS算法已經選擇了知足rethinkdb-go module指定要求所需的logrus module的「最小」版本。記住logrus module的「最新最大」版本是1.4.2。
注意:go.sum文件不該用於理解依賴關係。我在上面所作的版本肯定的操做是錯誤的,稍後我將向您展現肯定項目所使用的版本的正確方法。
上圖顯示了Go將使用哪一個版本的logrus module來構建項目。
接下來,我將添加引入對golib module有依賴關係的代碼。
清單6:https://play.golang.org/p/h23opcp5qd0
01 package main0203 import (04 "context"05 "log"0607 "github.com/Bhinneka/golib"08 db "gopkg.in/rethinkdb/rethinkdb-go.v5"09 )1011 func main() {12 c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)13 if err != nil {14 log.Fatalln(err)15 }1617 if _, err = c.Query(context.Background(), db.Query{}); err != nil {18 log.Fatalln(err)19 }2021 golib.CreateDBConnection("")22 }
清單6向該程序添加了07和21行行代碼。Go查找、下載並解壓縮golib module後,如下更改將顯示在go.mod文件中。
清單7:
01 module app0203 go 1.130405 require (06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba07 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.108 )
清單7顯示go.mod文件已被修改成包括golib module的「最新最大」版本依賴關係,該版本剛好沒有語義版本標籤。
清單8:
...github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=...
清單8顯示了go.sum文件中的四行,如今包括logrus module的v1.0.6和v1.2.0版本。查看go.sum文件中列出的兩個版本會帶來兩個問題:
•爲何在go.sum文件中列出了兩個版本?•Go執行構建時將使用哪一個版本?
Go團隊的Bryan Mills很好地回答了go.sum文件中列出兩個版本的緣由。
「go.sum文件仍包含舊版本(1.0.6),由於其傳遞依賴的要求可能會影響其餘module的選定版本。咱們真的只須要爲go.mod文件提供校驗和,由於go.mod中聲明瞭這些傳遞要求的內容,可是因爲go mod tidy不夠精確,最終咱們也保留了源代碼的校驗和。」 golang.org/issue/33008
如今仍然存在在構建項目時將使用哪一個版本的logrus module的問題。要正確肯定將使用哪些module及其版本,請不要查看該go.sum文件,而應使用go list命令。
清單9:
$ go list -m all | grep logrus
github.com/sirupsen/logrus v1.2.0
清單9顯示了在構建項目時將使用logrus module的v1.2.0版本。該-m標誌指示go list列出module而不是package。
查看module圖能夠更深刻地瞭解項目對logrus module的要求。
清單10:
$ go mod graph | grep logrus
github.com/sirupsen/logrus@v1.2.0 github.com/pmezard/go-difflib@v1.0.0github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/objx@v0.1.1github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/testify@v1.2.2github.com/sirupsen/logrus@v1.2.0 golang.org/x/crypto@v0.0.0-20180904163835-0709b304e793github.com/sirupsen/logrus@v1.2.0 golang.org/x/sys@v0.0.0-20180905080454-ebe1bf3edb33gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6github.com/sirupsen/logrus@v1.2.0 github.com/konsorten/go-windows-terminal-sequences@v1.0.1github.com/sirupsen/logrus@v1.2.0 github.com/davecgh/go-spew@v1.1.1github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
清單10顯示了logrus module在項目中的關係。我將直接提取顯示對logrus的依賴要求的行。
清單11:
gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
在清單11中,這些行顯示三個module(rethinkdb-go,golib和common)都須要logrus module。因爲有了go list命令,我知道所需的最低版本爲v1.2.0。
上圖展現了Go如今將使用哪一個版本的logrus module來構建項目中的代碼。
在將代碼提交/推回存儲庫以前,請運行go mod tidy以確保module文件是最新且準確的。您在本地構建,運行或測試的代碼將隨時影響Go對module文件中內容的更新。運行go mod tidy將確保項目具備所需內容的準確和完整的快照,這將幫助您團隊中的其餘人和您的CI/CD環境。
清單12:
$ go mod tidy
go: finding github.com/Bhinneka/golib latestgo: finding github.com/bitly/go-hostpool latestgo: finding github.com/bmizerany/assert latest
清單12顯示了運行go mod tidy後的輸出結果。您會在輸出中看到兩個新的依賴項。這將更改module文件。
清單13:
01 module app0203 go 1.130405 require (06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba07 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect08 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect09 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.110 )
清單13顯示了go-hostpool和assert module被列爲構建項目所需的間接module。之因此在此處列出它們,是由於這些項目當前與module機制不兼容。換句話說,這些項目的任何tag版本或master中「最新的」版本都不存在go.mod文件。
爲何運行go mod tidy後包含了這些module?我可使用go mod why命令找出答案。
清單14:
$ go mod why github.com/hailocab/go-hostpool
# github.com/hailocab/go-hostpoolapp/cmd/dbgopkg.in/rethinkdb/rethinkdb-go.v5github.com/hailocab/go-hostpool
------------------------------------------------
$ go mod why github.com/bmizerany/assert
# github.com/bmizerany/assertapp/cmd/dbgopkg.in/rethinkdb/rethinkdb-go.v5github.com/hailocab/go-hostpoolgithub.com/hailocab/go-hostpool.testgithub.com/bmizerany/assert
清單14顯示了爲何項目間接須要這些module。rethinkdb-go module須要go-hostpool module,而go-hostpool module須要assert module。
該項目具備三個依賴項,每一個依賴項都須要logrus module,其中當前正在選擇logrus module的v1.2.0版本。在項目生命週期的某個時刻,升級直接和間接依賴關係以確保項目所需的代碼是最新的而且能夠利用新功能、錯誤修復和升級安全補丁將變得很重要。要進行升級,Go提供了go get命令。
在運行go get升級項目的依賴項以前,須要考慮幾個選項。
我建議從這種升級開始,直到您瞭解更多有關項目和module的信息。這是的最保守的形式go get。
清單15:
$ go get -t -d -v ./...
清單15顯示瞭如何使用MVS算法對那些必需依賴項的升級。下面是命令中一些命令行選型的定義。
•-t flag:考慮構建測試所需的module。•-d flag:下載每一個module的源代碼,但不要構建或安裝它們。•-v flag:提供詳細輸出。•./... :在整個源代碼樹中執行這些操做,而且僅更新所需的依賴項。
對當前項目運行此命令不會致使任何更改,由於該項目已是最新版本,而且具備構建和測試該項目所需的最低版本。那是由於我剛運行了go mod tidy,項目是新的。
這種升級會將整個項目的依賴性從「最小」提升到「最新最大」。所須要作的只是將-u標誌添加到命令行。
清單16:
$ go get -u -t -d -v ./...
go: finding golang.org/x/net latestgo: finding golang.org/x/sys latestgo: finding github.com/hailocab/go-hostpool latestgo: finding golang.org/x/crypto latestgo: finding github.com/google/jsonapi latestgo: finding gopkg.in/bsm/ratelimit.v1 latestgo: finding github.com/Bhinneka/golib latest
清單16顯示了運行帶有-u標誌的go get命令的輸出。此輸出沒法說明真實狀況。若是我問go list命令如今使用哪一個版本的logrus module來構建項目,會發生什麼狀況呢?
清單17:
$ go list -m all | grep logrus
github.com/sirupsen/logrus v1.4.2
清單17顯示瞭如何選擇「最新」的logrus。爲了使這一選擇更加明確,對go.mod文件進行了更改。
清單18:
01 module app0203 go 1.130405 require (06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba07 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect08 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect09 github.com/cenkalti/backoff v2.2.1+incompatible // indirect10 github.com/golang/protobuf v1.3.2 // indirect11 github.com/jinzhu/gorm v1.9.11 // indirect12 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect13 github.com/sirupsen/logrus v1.4.2 // indirect14 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect15 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect16 golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect17 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.118 )
清單18在第13行顯示版本v1.4.2如今是項目中logrus module的選定版本。構建項目時,Go會注意module文件中的這一行。即便刪除了對logrus module的依賴關係更改的代碼,該項目的v1.4.2版如今也已被鎖定。請記住,降級將是一個更大的變化,而v1.4.2版將不受影響。
go.sum文件中能夠看到哪些更改?
清單19:
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
清單19顯示了go.sum文件中表示logrus的全部三個版本。正如上面的Bryan所解釋的,這是由於傳遞要求可能會影響其餘module的選定版本。
上圖展現了Go如今將使用哪一個版本的logrus module來構建項目中的代碼。
您能夠將./...選項替換爲all來升級全部直接和間接依賴項,包括構建項目時也並不須要的依賴項。
清單20:
$ go get -u -t -d -v all
go: downloading github.com/mattn/go-sqlite3 v1.11.0go: extracting github.com/mattn/go-sqlite3 v1.11.0go: finding github.com/bitly/go-hostpool latestgo: finding github.com/denisenkom/go-mssqldb latestgo: finding github.com/hailocab/go-hostpool latestgo: finding gopkg.in/bsm/ratelimit.v1 latestgo: finding github.com/google/jsonapi latestgo: finding golang.org/x/net latestgo: finding github.com/Bhinneka/golib latestgo: finding golang.org/x/crypto latestgo: finding gopkg.in/tomb.v1 latestgo: finding github.com/bmizerany/assert latestgo: finding github.com/erikstmartin/go-testdb latestgo: finding gopkg.in/check.v1 latestgo: finding golang.org/x/sys latestgo: finding github.com/golang-sql/civil latest
清單20顯示瞭如今爲該項目找到、下載和提取了多少個依賴項。
清單21:
Added to Module File cloud.google.com/go v0.49.0 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect github.com/google/go-cmp v0.3.1 // indirect github.com/jinzhu/now v1.1.1 // indirect github.com/lib/pq v1.2.0 // indirect github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect github.com/onsi/ginkgo v1.10.3 // indirect github.com/onsi/gomega v1.7.1 // indirect github.com/stretchr/objx v0.2.0 // indirect google.golang.org/appengine v1.6.5 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.7 // indirect
Removed from Module File github.com/golang/protobuf v1.3.2 // indirect
清單21顯示了對該go.mod文件的更改。添加了更多module,並刪除了一個module。
注意:若是你使用vendor,則go mod vendor命令將從vendor文件夾中剝離test文件。
一般,經過go get升級項目的依賴項時不要使用all或-u選項。堅持只升級須要的module,並使用MVS算法選擇這些module及其版本。必要時手動更改成特定的module版本。手動更改能夠經過手動編輯go.mod文件來完成,我將在之後的文章中向您展現。
若是您在任什麼時候候都不滿意所選的module和版本,則你始終能夠經過刪除module文件並再次運行go mod tidy來重置選擇。當項目還很年輕而且狀況不穩定時,這更是一種選擇。項目穩定併發布後,我會猶豫從新設置依賴關係。正如我上面提到的,隨着時間的推移,可能會設置module版本,而且您須要長期持久且可重複的構建。
清單22:
$ rm go.*$ go mod init <module name>$ go mod tidy
清單22顯示了容許MVS從頭開始再次執行全部選擇的命令。在撰寫本文的整個過程當中,我一直在進行此操做以重置項目並提供本文的代碼清單。
在這篇文章中,我解釋了MVS語義,並展現了Go和MVS算法實際應用的真實示例。我還展現了一些Go命令,這些命令能夠在您遇到未知問題時爲您提供信息。在爲項目添加愈來愈多的依賴項時,可能會遇到一些極端狀況。這是由於Go生態系統已有10年的歷史,全部現有項目都須要更多時間才能符合module要求。
在之後的文章中,我將討論在同一項目中使用不一樣主要版本的依賴關係,以及如何手動檢索和鎖定依賴關係的特定版本。如今,我但願您對module和Go工具備更多的信任,而且對MVS如何隨着時間的推移選擇版本有了更清晰的瞭解。若是您遇到任何問題,能夠在#module組的Gopher Slack上找到一羣願意提供幫助的人。