還有半個月go1.12就要發佈了。這是首個將go modules歸入正式支持的穩定版本。html
距離go modules隨着go1.11正式面向廣大開發者進行體驗也已通過去了半年,這段時間go modules也發生了一些變化,藉此機會我想再次深刻探討go modules的使用,同時對這個新生包管理方案作一些思考。git
本文索引
包的版本控制老是一個包管理器繞不開的古老話題,天然對於咱們的go modules也是這樣。github
咱們將學習一種新的版本指定方式,而後深刻地探討一下golang官方推薦的semver
即語義化版本。golang
在討論go get進行包管理時咱們曾經討論過如何對包版本進行控制(文章在此),支持的格式以下:chrome
vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef vX.0.0-yyyymmddhhmmss-abcdefabcdef vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef vX.Y.Z
在go.mod文件中咱們也須要這樣指定,不然go mod沒法正常工做,這帶來了2個痛點:npm
基於以上緣由,如今能夠直接使用commit的hash來指定版本,以下:json
# 使用go get時 go get github.com/mqu/go-notify@ef6f6f49 # 在go.mod中指定 module my-module require ( // other packages github.com/mqu/go-notify ef6f6f49 )
隨後咱們運行go build
或go mod tidy
,這兩條命令會整理並更新go.mod文件,更新後的文件會是這樣:api
module my-module require ( github.com/mattn/go-gtk v0.0.0-20181205025739-e9a6766929f6 // indirect github.com/mqu/go-notify v0.0.0-20130719194048-ef6f6f49d093 )
能夠看到hash信息自動擴充成了符合要求的版本信息,從此能夠依賴這一特性簡化包版本的指定。緩存
對於hash信息只有兩個要求:安全
v
,只須要給出commit hash便可然而這和咱們理想中的版本控制方式彷佛仍是有些出入,是否是以爲。。。有點不直觀?接下來介紹的語義化版本也許能帶來一些改觀。
golang官方推薦的最佳實踐叫作semver
,這是一個簡稱,寫全了就是Semantic Versioning
,也就是語義化版本。
通俗地說,就是一種清晰可讀的,明確反應版本信息的版本格式,更具體的規範在這裏。
如規範所言,形如vX.Y.Z
的形式顯然比一串hash更直觀,因此golang的開發者纔會把目光集中於此。
semver
簡化版本指定的做用是顯而易見的,然而僅此一條理由顯然有點缺少說服力,畢竟改進後的版本指定其實也不是那麼麻煩,對吧?
那麼爲什麼要引入一套新的規範呢?
我想這可能與golang一向重視工程化的哲學有關:
不要刪除導出的名稱,鼓勵標記的複合文字等等。若是須要不一樣的功能,添加 新名稱而不是更改舊名稱。若是須要完整中斷,請建立一個帶有新導入路徑的新包。 -go modules wiki
經過semver
對版本進行嚴格的約束,能夠最大程度地保證向後兼容以及避免「breaking changes」,而這些都是golang所追求的。二者一拍即合,因此go modules提供了語義化版本的支持。
若是你使用和發佈的包沒有版本tag或者處於1.x版本,那麼你可能體會不到什麼區別,由於go mod所支持的格式從始至終是遵循semver
的,主要的區別體如今v2.0.0
以及更高版本的包上。
「若是舊軟件包和新軟件包具備相同的導入路徑,則新軟件包必須向後兼容舊軟件包。」 - go modules wiki
正如這句話所說,相同名字的對象應該向後兼容,然而按照語義化版本的約定,當出現v2.0.0
的時候必定表示發生了重大變化,極可能沒法保證向後兼容,這時候應該如何處理呢?
答案很簡單,咱們爲包的導入路徑的末尾附加版本信息便可,例如:
module my-module/v2 require ( some/pkg/v2 v2.0.0 some/pkg/v2/mod1 v2.0.0 my/pkg/v3 v3.0.1 )
格式總結爲pkgpath/vN
,其中N
是大於1的主要版本號。在代碼裏導入時也須要附帶上這個版本信息,如import "some/pkg/v2"
。如此一來包的導入路徑發生了變化,也不用擔憂名稱相同的對象須要向後兼容的限制了,由於golang認爲不一樣的導入路徑意味着不一樣的包。
不過這裏有幾個例外能夠不用參照這種寫法:
gopkg.in
格式時可使用等價的require gopkg.in/some/pkg.v2 v2.0.0
+incompatible
就能夠不須要指定/vN
,例如:require some/pkg v2.0.0+incompatible
GO111MODULE=off
將取消這種限制,固然go1.12裏就不能這麼幹了除此之外的狀況若是直接使用v2+版本將會致使go mod報錯。
v2+版本的包容許和其餘不一樣大版本的包同時存在(前提是添加了/vN
),它們將被當作不一樣的包來處理。
另外/vN
並不會影響你的倉庫,不須要建立一個v2對應的倉庫,這只是go modules添加的一種附加信息而已。
固然若是你不想遵循這一規範或者須要兼容現有代碼,那麼指定+incompatible
會是一個合理的選擇。不過如其字面意思,go modules不推薦這種行爲。
眼尖的讀者可能已經發現了,semver
很眼熟。
是的,REST api
是它的最忠實用戶,像xxx.com/api/v2/xxx
的最佳實踐咱們恐怕都司空見慣了,因此golang纔會要求v2+的包使用pkg/v2
的形式。然而把REST api
的最佳實踐融合進包管理器設計,真的會是又一個最佳實踐嗎?
我以爲未必如此,一個顯而易見的缺點就在於向後兼容上,主流的包管理器都只採用semver
的子集,最大的緣由在於若是隻提供對版本的控制,而把前後兼容的責任交由開發者/用戶相對於強行將無關的信息附加在包名上來講可能會形成必定的迷惑,可是這種作法能夠最大限度的兼容現有代碼,而golang則須要修改mod文件,修改引入路徑,分散的修改每每致使潛在的缺陷,考慮到現有的golang生態這一作法顯得不那麼明智。同時將版本信息綁定進包名對於習慣了傳統包管理器方案的用戶(npm,pip)來講顯得有些怪異,可能須要花上一些額外時間適應。
不過檢驗真理的標準永遠都是實踐,隨着go1.12的發佈咱們最終會見分曉,對於go modules如今是給予耐心提出建議的階段,評判還爲時尚早。
go mod edit -replace
無疑是一個十分強大的命令,但強大的同時它的限制也很是多。
本部分你將看到兩個例子,它們分別闡述了本地包替換的方法以及頂層依賴與間接依賴的區別,如今讓咱們進入第一個例子。
replace除了能夠將遠程的包進行替換外,還能夠將本地存在的modules替換成任意指定的名字。
假設咱們有以下的項目:
tree my-mod my-mod ├── go.mod ├── main.go └── pkg ├── go.mod └── pkg.go
其中main.go負責調用my/example/pkg
中的Hello
函數打印一句「Hello」,my/example/pkg
顯然是個不存在的包,咱們將用本地目錄的pkg
包替換它,這是main.go:
package main import "my/example/pkg" func main() { pkg.Hello() }
咱們的pkg.go相對來講很簡單:
package pkg import "fmt" func Hello() { fmt.Println("Hello") }
重點在於go.mod文件,雖然不推薦直接編輯mod文件,但在這個例子中與使用go mod edit
的效果幾乎沒有區別,因此你能夠嘗試本身動手修改my-mod/go.mod:
module my-mod require my/example/pkg v0.0.0 replace my/example/pkg => ./pkg
至於pkg/go.mod,使用go mod init
生成後不用作任何修改,它只是讓咱們的pkg成爲一個module,由於replace的源和目標都只能是go modules。
由於被replace的包首先須要被require(wiki說本地替換不用指定,然而我試了報錯),因此在my-mod/go.mod中咱們須要先指定依賴的包,即便它並不存在。對於一個會被replace的包,若是是用本地的module進行替換,那麼能夠指定版本爲v0.0.0
(對於沒有使用版本控制的包只能指定這個版本),不然應該和替換包的指定版本一致。
再看replace my/example/pkg => ./pkg
這句,與替換遠程包時同樣,只是將替換用的包名改成了本地module所在的絕對或相對路徑。
一切準備就緒,咱們運行go build
,而後項目目錄會變成這樣:
tree my-mod my-mod ├── go.mod ├── main.go ├── my-mod └── pkg ├── go.mod └── pkg.go
那個叫my-mod的文件就是編譯好的程序,咱們運行它:
./my-mod Hello
運行成功,my/example/pkg
已經替換成了本地的pkg
。
同時咱們注意到,使用本地包進行替換時並不會生成go.sum所需的信息,因此go.sum文件也沒有生成。
本地替換的價值在於它提供了一種使自動生成的代碼進入go modules系統的途徑,畢竟無論是go tools仍是rpc工具,這些自動生成代碼也是項目的一部分,若是不能歸入包管理器的管理範圍想必會帶來很大的麻煩。
若是你由於golang.org/x/...
沒法獲取而使用replace進行替換,那麼你確定遇到過問題。明明已經replace的包爲什麼還會去未替換的地址進行搜索和下載?
解釋這個問題前先看一個go.mod的例子,這個項目使用的第三方模塊使用了golang.org/x/...
的包,但項目中沒有直接引用它們:
module schanclient require ( github.com/PuerkitoBio/goquery v1.4.1 github.com/andybalholm/cascadia v1.0.0 // indirect github.com/chromedp/chromedp v0.1.2 golang.org/x/net v0.0.0-20180824152047-4bcd98cce591 // indirect )
注意github.com/andybalholm/cascadia v1.0.0
和golang.org/x/net v0.0.0-20180824152047-4bcd98cce591
後面的// indirect
,它表示這是一個間接依賴。
間接依賴是指在當前module中沒有直接import,而被當前module使用的第三方module引入的包,相對的頂層依賴就是在當前module中被直接import的包。若是兩者規則發生衝突,那麼頂層依賴的規則覆蓋間接依賴。
在這裏golang.org/x/net
被github.com/chromedp/chromedp
引入,但當前項目未直接import,因此是一個間接依賴,而github.com/chromedp/chromedp
被直接引入和使用,因此它是一個頂層依賴。
而咱們的replace命令只能管理頂層依賴,因此在這裏你使用replace golang.org/x/net => github.com/golang/net
是沒用的,這就是爲何會出現go build時仍然去下載golang.org/x/net
的緣由。
那麼若是我把// indirect
去掉了,那麼不就變成頂層依賴了嗎?答案固然是不行。無論是直接編輯仍是go mod edit
修改,咱們爲go.mod添加的信息都只是對go mod
的一種提示而已,當運行go build
或是go mod tidy
時golang會自動更新go.mod致使某些修改無效,簡單來講一個包是頂層依賴仍是間接依賴,取決於它在本module中是否被直接import,而不是在go.mod文件中是否包含// indirect
註釋。
replace惟一的限制是它只能處理頂層依賴。
這樣限制的緣由也很好理解,由於對於包進行替換後,一般不能保證兼容性,對於一些使用了這個包的第三方module來講可能意味着潛在的缺陷,而容許頂層依賴的替換則意味着你對本身的項目有充足的自信不會由於replace引入問題,是可控的。至關符合golang的工程性原則。
也正如此replace的適用範圍受到了至關的限制:
import "./mypkg"
,因此須要考慮replace除此以外的replace暫時沒有什麼用處,固然之後若是有變更的話說不定能夠發揮比如今更大的做用。
發佈go modules
也許你知道npm的package-lock.json的做用,它會記錄全部庫的準確版本,來源以及校驗和,從而幫助開發者使用正確版本的包。一般咱們發佈時不會帶上它,由於package.json已經夠用,而package-lock.json的內容過於詳細反而會對版本控制以及變動記錄等帶來負面影響。
若是看到go.sum文件的話,也許你會以爲它和package-lock.json同樣也是一個鎖文件,那就大錯特錯了。go.sum不是鎖文件。
更準確地來講,go.sum是一個構建狀態跟蹤文件。它會記錄當前module全部的頂層和間接依賴,以及這些依賴的校驗和,從而提供一個能夠100%復現的構建過程並對構建對象提供安全性的保證。
go.sum同時還會保留過去使用的包的版本信息,以便往後可能的版本回退,這一點也與普通的鎖文件不一樣。因此go.sum並非包管理器的鎖文件。
所以咱們應該把go.sum和go.mod一同添加進版本控制工具的跟蹤列表,同時須要隨着你的模塊一塊兒發佈。若是你發佈的模塊中不包含此文件,使用者在構建時會報錯,同時還可能出現安全風險(go.sum提供了安全性的校驗)。
golang一直提供了工具選擇上的自由性,若是你不喜歡go mod的緩存方式,你可使用go mod vendor
回到godep
或govendor
使用的vendor
目錄進行包管理的方式。
固然這個命令並不能讓你從godep之類的工具遷移到go modules,它只是單純地把go.sum中的全部依賴下載到vendor目錄裏,若是你用它遷移godep你會發現vendor目錄裏的包回合godep指定的產生至關大的差別,因此請務必不要這樣作。
咱們舉第一部分中用到的項目作例子,使用go mod vendor
以後項目結構是這樣的:
tree my-module my-module ├── go.mod ├── go.sum ├── main.go └── vendor ├── github.com │ ├── mattn │ │ └── go-gtk │ │ └── glib │ │ ├── glib.go │ │ └── glib.go.h │ └── mqu │ └── go-notify │ ├── LICENSE │ ├── README │ └── notify.go └── modules.txt
能夠看到依賴被放入了vendor目錄。
接下來使用go build -mod=vendor
來構建項目,由於在go modules模式下go build是屏蔽vendor機制的,因此須要特定參數從新開啓vendor機制:
go build -mod=vendor ./my-module a notify!
構建成功。當發佈時也只須要和使用godep時同樣將vendor目錄帶上便可。
其實這是第一部分的老生常談,當你發佈一個v2+版本的庫時,須要進行如下操做:
module my-module
改爲module my-module/v2
import "my-module"
改成import "my-module/v2"
my-module
包的版本是否統一,修改那些不兼容的問題+incompatible
是一個暫時性的解決方案。注意以上幾點的話發佈go modules也就是一個輕鬆的工做了。
相比godep和vendor機制而言,go modules已是向現代包管理器邁出的堅實一步,雖然還有很多僵硬甚至詭異的地方,可是我的仍是推薦在go1.12發佈後考慮逐步遷移到go modules,畢竟有官方的支持,相關issues的討論也很活躍,不出意外應該是go包管理方案的最終答案,如今花上一些時間是值得的。
固然包管理是一個很大的話題,就算本文也只是講解了其中的一二,之後我也許有時間會介紹更多go modules相關的內容。
總之go modules仍是一個新興事物,包管理器是一個須要不斷在實踐中完善的工具,若是你有建設性的想法請儘可能向官方反饋。
go modules的官方wiki也上線一段時間了,這篇文件基本上是與其結合的查漏補缺,同時也夾雜了一些我的看法,因此不免有所錯誤疏漏,歡迎指正。