Go語言入門——dep入門
本文出現了大量maven的內容,更適合java程序員閱讀,如果你的語言做依賴管理的方案與maven差異很大,可能在有些地方會不理解
從很久之前go語言在依賴解決和管理方面方案的匱乏就被不少人詬病。光指望go get指令,很多事辦不成。我也不清楚從什麽時候開始,dep,這個官方的解決方案開始被推廣了。從說明上看,不會早於go 1.8,從github的源代碼上看,至少開源不會超過1年
官方對於dep的介紹是“dep is the official experiment, but not yet the official tool.” 但又說 “dep is safe for production use.”。我並沒在實踐中真實使用,但也做了不少試驗,基本可以認定其穩定,且“知名”,未來如果有更好的依賴管理方案,十有八九也是上面加個UI的殼吧
當然還有一個問題擺在面前:jenkins似乎沒有dep的插件?那只能靠手寫腳本了
在往下看之前先確定你清楚兩個術語:我們把 github.com/apodemakeles/ugo 這玩意叫project,這是一個github上的倉庫,也代表一個項目,把github.com/apodemakeles/ugo/time 這玩意兒叫package,是project下面一個go 語言的包。current project表示當前你正在編寫的項目
前置知識點
首先你要清楚,go中獲取依賴包都是通過執行go get命令,通過解析代碼中的import語句,去下載相應的源代碼到$GOPATH/src下,然後再進行install,安裝在$GOPATH/pkg下。所以你如果要對應maven中下載的java的jar包的話,那實際上等於go中的源碼和一堆.a文件。go get的說明
但go get的問題是沒有版本上的控制,今天你運行的代碼,可能和明天在jenkins上生成的代碼完全不一樣
另外一個問題也是拜go語言的特性所賜,由於所有的依賴項必須在$GOPATH下有對應的源碼和.a文件,所以在同一臺機上的不同項目,沒法使用同一依賴項的不同版本。難道你要在不同的項目編譯時git checkout一下?
所以在go 1.5開始官方引入了go vendor機制,簡單點說就是在原來的project目錄下加一個vendor文件夾,源碼都“照搬”到這裏,目錄結構不動。所有import優先使用vendor中的源碼。這個口子一開,一時間第三方紛紛推出自己的依賴解決工具了,比如我以前用過的govendor。 go vendor的說明
dep一瞥
要使用dep,首先要去github下載dep源碼,編譯成本地的可執行文件。確保你的$GOPATH/bin在你的PATH中,這樣就可以在命令行執行dep命令了。具體詳見 github 和 dep官方文檔 ,不在此贅述
隨便建立一個項目,當然要在$GOPATH下,此時先不要寫代碼,至少不要import遠程的依賴,執行
dep init
你能看到project路徑下多了三個東西,一個vendor文件夾,這個未來要來放current project的遠程依賴的源代碼,一個Gopkg.toml文件,你可以暫時將其類比為pom文件,或者npm的package.json文件,還有一個Gopkg.lock文件,這是啥?
既然toml文件類似於pom或者package.json,那按照裏面的註釋,試著添加一個依賴項試試,比如像我一樣
[[constraint]] name = "github.com/apodemakeles/ugo" version = "=0.1.0"
看起來意思是需要下載ugo這個project,並且限制版本為0.1.0,下面執行
dep ensure
這個指令類似於install,compile之類,就是根據依賴的配置內容,下載依賴,編譯依賴。指令很快就執行完了,但什麽也沒發生?這就是我之前說“暫時”類比為pom文件的原因,dep中要結合toml和代碼中的import語句,才會真的下載,編譯依賴項。把下面的代碼貼到項目中去
package main import ( "fmt" "github.com/apodemakeles/ugo/time" ) func main() { fmt.Println(utime.NowUnixTS()) }
重新執行dep ensure,你會看到vendor中有了源代碼,而那個不知道是什麽的lock文件裏是這樣的:
# This file is autogenerated, do not edit; changes may be undone by the next ‘dep ensure‘. [[projects]] name = "github.com/apodemakeles/ugo" packages = ["time"] revision = "96e9671d8beda19466b4296a8939ebfe26210683" version = "v0.1.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "4b0b8768bb38a412e1bbfd9952fe578e6f5b1a7469e3f44e444d66ca0c7ffaf6" solver-name = "gps-cdcl" solver-version = 1
現在再正式解釋toml文件和lock文件:
toml文件記錄著current project依賴項project的約束,而並不是應該有哪些project,有哪些project還是要看import了哪些package。這個約束主要體現在到底要采用目標project的某個tag的版本(version),還是某個branch,或者是某個commit sha1(revision),後面我們會稱其"type", 這三個對於一個constraint只能選一個。你可以試試去掉toml的內容,執行dep ensure,依舊可以下載文件到vendor,依舊會修改lock文件
實際上除了constraint,還有其他幾個約束, 比較重要的有required,ignored,override,前兩個本文不會重點說明,override會在後面重點說明。toml文件的說明
lock文件是工具生成的,你不應該手工編輯,lock文件的packages對應你import的內容,而revision(一定會有,和type無關)和version則為vendor中源碼的真實反映。lock文件的說明
註意到version那項寫的是"v0.1.0"了沒有?這是我github上代碼真正的tag,在toml文件中的約束忽略了首字母v,直接拉取了tag為v0.1.0的代碼
接下來把toml中的version改為“=0.1.1” 試試,假設你需要更高版本ugo的一個功能,需要升級依賴。執行完dep ensure你會發現vendor中變化了,lock文件也變了
以上就是dep的簡略介紹,我建議大家看一下 dep的運行機制 。裏面提到了dep ensure這個命令,可以類比為函數的執行:toml和import就好比一個函數的輸入,經過第一個函數resolving,輸出的是lock文件,把其當做輸入傳入到第二個函數vendoring,輸出的是vendor文件夾中的內容。這樣有助於大家的理解,在本文沒涉及到的情況,可以自己推理出來
模擬maven的一個方案
以下是我假象的,沒經過驗證的一個實際開發中的工作流程
首先,最好確保你的項目使用semver標準——語義化的版本標準,semver說明 ,這裏規定了版本號代表的意思,比較,以及一些操作符。還規定最初版本從0.1.0開始(看到此一陣驚喜,我們組蒙對了)
然後,了解你依賴的代碼,需要使用哪個版本。如果是自己團隊的類庫,遵守semver標準,利用git的tag功能來表示version, 比如打一個v0.1.0的標簽
之後規規矩矩的按照go的要求新建一個項目,並且搞定版本控制。在ignore中寫上 vendor/。這裏我曾疑問,lock是否也可以排除?實際上可以,但官方文檔曾經提到過"commit" lock文件
在項目根目錄執行dep init。生成這三個東西。如果你可以copy一份toml過來,dep init完全不用執行,只要有toml文件,dep ensure完全可以生成其他兩個
之後編碼,涉及到遠程依賴的內容,先在import中導入,再選擇對應版本,在toml中修改,之後執行dep ensure
如果開發過程中需要升級,修改toml文件,再執行dep ensure
CI工具拉去到代碼後,由於有toml文件,直接執行dep ensure就可以了,解決完依賴最後再執行go build
這麽看起來似乎全局只有這一個命令是必須
註意,在我建議的方案中,toml文件要寫成這樣 version=“0.1.0”,不要再0.1.0前加等於號,為什麽這樣,你先最好了解了解go dep 的version rule
version rules
version rules在此,簡單說,三位版本號第一位為major,跨major可以不兼容,後兩位為minor和patch,必須保證在同一個major範圍內向後兼容。說白了我以前用1.2.1版本,現在換成1.2.2或者1.3.1了,一定要沒錯誤,但2.1.0不行(這就是咱們架構組的規則)
在dep中, =0.1.0代表確定這個版本, ^0.1.0代表 >=0.1.0 且 <1.0.0
[[constraint]] name = "github.com/apodemakeles/ugo" version = "0.1.0"
這種寫法等價於^0.1.0,默認的一般是推薦的方案,為什麽推薦這種呢?你可以理解,如果你按照semver的規範,沒跨major的一定向後兼容,所以即使獲取到0.1.1, 0.2.0,也不會出錯。可為什麽不能像maven一樣固定一個坐標呢?
傳遞依賴
假設A依賴於B的一個功能(A,B是project, jar,或者dll),我們用A->B來表示,如果有A->B->C,則B對C為直接依賴,A對C為傳遞依賴。如果恰巧A->B->C且A->C呢,但這兩個C又不是一個版本,會發生什麽?
在node.js中並沒有這種困擾,在依賴文件夾中會有兩個C存在,然而對於C#,Java,Go這些語言,他們共同特點是current project下最終一個C只會有一個真實在磁盤上的產物,(dll, jar,帶路徑的.a),這時候就發生了依賴沖突問題
在C#的MSBuild中,隨便選一個C(我感覺總是選高版本),生成dll,但在運行時,任何一個用到C.dll的dll,會檢查當前依賴的版本範圍內有沒有C的版本,如果沒有則在運行時出錯
在Java的Maven中,會采用“最短路徑”原則,此時A->C這條路徑比較近,采用這條路徑的pom中的版本。但這就又引來一個問題,如果這條短路徑的C版本比較低,恰好B要用一個更高的C版本,因為裏面有一個新方法,在運行時就會出錯
那Go的dep呢?如果你在toml文件中這麽寫
[[constraint]] name = "github.com/apodemakeles/B" version = "=xxx" [[constraint]] name = "github.com/apodemakeles/C" version = "=0.1.1"
此時不管B->C的版本比0.1.1高還是低(B->C也寫為version="=x.x.x"),dep都不允許,都會報錯,理由是“has no overlap”。如果B->C和A->C在一個major中,那就放開吧,直接寫 verion="x.x.x"
這就是我推薦不帶等號的原因
但有時候就是要確定某一個版本怎麽辦?可以使用toml中另一個約束override
[[constraint]] name = "github.com/apodemakeles/B" version = "=xxx" [[override]] name = "github.com/apodemakeles/C" version = "=0.1.1"
這樣會強制使用0.1.1的版本的C,但有可能出現上面maven同樣的情況,需要你自己負責
零零散散的細節
截止到這裏基本我覺得重要的內容就都說完了,toml和lock中還有很多內容沒說,比如required,ignored,這些東西自己看就好。還有一些dep ensure的參數,比如-update,-add,我覺得大家不知道更好,可以統一使用規範,就只用dep ensure。而且官方也不建議把branch或者revision作為版本控制
- 如果你想要一個0.0.1版本的project,而服務器只有0.1.0以上的版本,即使使用範圍比如^,~,也獲取不到。我猜是因為dep把0.1.0作為最初始的版本
- dep status可以查看當前依賴信息
- dep ensure真挺慢
Go語言入門——dep入門