Go包管理的前世今生
說實話,Golang對一個新人真的挺不友善的,因為一上手要了解的概念。你看人家Java,上來一個項目mvn install一下就完事了,干凈利落。但是Golang就麻煩了,你得先了解什么是GOPATH。我當年剛接觸Golang真正開始做項目的時候,只知道按要求配置環境變量,對GOPATH真正理解可能都是好幾個月以后的事情了。說白了,還是因為懶。真正做項目的人,有多少有耐心砍柴磨刀,出現一個東西就研究半天啊,我們只是想要Copy-Paste而已。
但是不得不承認,對于今天討論的Go包管理的話題,如果你想理解Golang的包管理機制,連GOPATH都不想充分理解一下,那可能真的不需要看這篇文章,下次遇到了照著README一步步老老實實來就行了。照著README文檔搞不定怎么辦?給項目維護者提BUG啊!
GOPATH
言歸正傳,還是回到GOPATH的理解上來。那么,GOPATH是什么?有什么用?本質上GOPATH是一個系統的環境變量,就是Go語言用來存放代碼依賴的地方。
很多人搞不清 GOPATH 、 GOROOT 的區別,其實沒必要理解的很復雜。當Go語言的安裝包剛下載完畢的時候,你把它解壓或者直接安裝到的那個目錄,就是 GOROOT 目錄,此時你需要做一些額外的配置,將GOROOT這個環境變量設置一下。當然,對應的bin目錄你也得設置一下,否則操作系統找不到go的執行文件。
比如你解壓后安裝到 /opt/go/ 目錄下了,又或者在Windows下面你安裝到了C盤 C:\\GO,都是OK的,區別只是不同操作系統環境下設置的方法不同而已。具體怎么設置我就不贅述了。
到此為止,你已經可以忘記 GOROOT 這個事情了,因為已經解決了所有跟它有關的事情。但是我們還是要解釋下,為什么要設置這個東西?道理很簡單,Golang的很多默認機制都很喜歡從環境變量里面去讀內容,設置了 GOROOT 環境變量,相當于告訴所有讀取這個變量的程序我們Golang的源碼位置,便于代碼的引用。可以理解為 GOROOT 就是三原色,用它可以組合出很多不同的色彩,是最初始的代碼依賴。GOROOT 里面的很多代碼都是系統驅動程序以及系統調用。
那么我們有了三原色,想要配出更多的顏色,我們調配顏色的過程中組合出的顏色,也就是新寫出來的代碼包放在哪里呢? 你一定已經猜到了,這就是 GOPATH 目錄的作用。所有Golang安裝包以外的代碼,無論是你自己寫的,還是第三方的無比成熟的包,都需要放置在GOPATH下面。
所以你不要再問為什么我直接 git clone 下來的代碼怎么各種報錯說找不到依賴啦,你設置 GOPATH 了嗎?
那么怎么設置 GOPATH 呢? 這就根據個人口味定了,很隨意。有的人選擇用隱藏目錄,比如 ~/.go/ 作為GOPATH,也有的人設置在GOROOT的隔壁,新建了個目錄叫/opt/gopath,對于Windows用戶來說也是如此,當然你完全可以設置環境變量把自己在自己的D盤下也創建一個叫gopath的目錄D:\\Gopath,然后設置下環境變量。而我個人則更偏愛/home/sunjianbo/gopath這個目錄,也就是~/gopath。
那么我可不可以有多個GOPATH目錄呢?當然可以,設置系統環境變量的時候就是可以放多個值的嘛。
你完全可以設置 export GOPATH=~/gopath1:~/gopath2:~/gopath3。在配置的這些目錄中,Go程序會依次去尋找有沒有對應的依賴包。
所以是不是有的讀者已經想明白最原始的包管理方法了呢?
公布答案,就是每個項目做一個 GOPATH。
具體而言,假設我們有個項目叫 tastego,我們在里面寫個腳步,內容如下:
export GOPATH=$PWD/tastego:$GOPATH
只要一行,簡單到任性。當然,最好再加一行,把GOPATH下的bin路徑加上,這樣go install出來的內容也能開箱即用。
export PATH=$PATH:$PWD/tastego/bin
對于每個項目的依賴,分門別類的放在對應的源碼目錄下。
tastego/
src/
github.com/
wonderflow/...
qiniu/...
golang.org/...
bin/
pkg/
所以,你可能不信,最開始Go官方根本沒有提供什么包管理機制的時候,好多Go語言玩家,都是用這樣的方式在玩。照樣玩的順風順水,數十上百萬行代碼不在話下。
但是值得一提的是,我們觀察到 GOPATH=$PWD/logkit:$GOPATH 這個結構是一個遞歸的結構,不僅可以這么寫,換個方式GOPATH=$GOPATH:$PWD/logkit 也是有效的。但是這兩種寫法的利弊是不同的。先說結論,這兩種方式我都不推薦。
- GOPATH=$PWD/logkit:$GOPATH 這個寫法是當前項目的目錄在前,所以遇到新的項目,永遠在gopath的最前面。這個很大的好處就是當你遇到多個包名稱完全相同的時候,用的默認是最前面的包,所以包沖突的概率會降到最低。壞處也顯而易見,你 go get 下來的代碼包,會進入到最前面的這個目錄,導致每次 go get的位置是不確定的,尤其是環境變量被無意中clean后,你甚至需要重新 go get一遍。
- GOPATH=$GOPATH:$PWD/logkit 這個寫法就保證了順序的穩定性,后來的包一定在后面,但是弊端就是你很快就要出現包版本沖突的問題了。
當然,上述做法還有一個巨大的缺點,就是當你的項目大了以后,你可能不得不把半個Github的代碼都放在你的單個代碼庫(Repo)下面。
百花齊放
終于,在Golang官方面對GOPATH管理的各種亂象始終無動于衷的時候,社區看不下去了,相繼出現各類包管理工具。
2013年的時候,大名鼎鼎的Gedep工具誕生了。這是社區第一個包管理工具,受到了大家的熱烈追捧。原理非常簡單,就是把我們上一節講的方式,通過腳本來實現。把所有的源碼保存到一個叫 Godeps/_workspace/ 的目錄下,然后將這個Godeps/_workspace/ 目錄作為唯一的 GOPATH. 提供了一系列包括 godep save 、 godep restore 這樣方便快捷的把所有依賴包都保存到 Godeps/_workspace/ 目錄下,同時又可以快速的通過godep 恢復 GOPATH,相當于對GOPATH及源碼的版本做了一次快照。當然,它并沒有解決你一次git clone需要下載半個github的難題。
但是就在這一年,Docker開始火起來了,所以好多Golang項目構建Dockerfile做測試的起手式就是:
ADD . /app/xxx/ ENV GOPATH /app/xxx/Godep/_workspace
一下子所有依賴全都有了,確實很方便。
但是在當時(2013~2014年),這個工具依然沒有解決我的版本問題,依然沒有解決我依賴的包如果還依賴許多別的包怎么辦的問題。于是到了2014年,號稱 參照了其他語言的包管理工具**最佳實踐** 的 glide 包管理工具誕生了。
那么它到底憑什么號稱自己是最佳實踐呢?它的最佳體現在它的包管理哲學上,一旦你安裝完成glide以后,一個glide create命令,就會完全搞定所有包依賴的問題。至于說構建時有多個版本可以用,依賴怎么選、選哪個的問題,它會幫你搞定。
那如果我希望自己指定包版本呢?當然也沒問題,只要在 glide.yaml 配置文件中填寫好你對應的包版本、或者版本的范圍,glide 會在這個范圍內選定。
所以對于從零構建項目的新用戶來說,glide 一定是個不錯的選擇,它幫你處理了所有的包依賴問題。但是對于一個已經有一些歷史包袱的項目來說,使用 glide 可能就有些尷尬了。尤其是你的項目里本來就存在一些依賴沖突的問題,glide 還非要幫你確定使用哪個依賴,導致最終還是運行不起來。
所以有人就問了,“可不可以不要絕對的給我最佳實踐,把我的依賴全導進來,一步步來行么?” 官方就說,"我們的設計哲學是glide管理下的每一步都是可執行的,不能只走半步!" 但是您沒給人解決問題啊,最終還是執行不了,這就很僵……
也許正是理念的不同,所以 glide 號稱“最佳實踐”但卻并未一統江湖。
這段時間還有諸如 gopkg.in、gom 等包管理工具誕生,其理念都比最初的godep先進,你說與 glide 比沒有更多特色,都加入了包版本的概念。
Vendor
時間走到了2015年,Golang官方終于看不下去了,在推出go1.5版本的同時,首次實驗性質的加入了 vendor機制 功能。當然,這個功能畢竟是實驗性質的,默認情況下是關閉的,導致大多數用戶實際上根本不會用它。直到2016年,在官方推出go1.6版本的時候,vendor機制 才默認變成開啟的狀態。
那么到底什么是 vendor機制 呢? 通俗的說,就是在你的項目中包含了一個vendor文件夾,go語言會把它默認為一個GOPATH。于是,你就可以在里面放你的依賴庫啦。
舉例來說,假設你托管在github上的項目是這樣的,項目名稱為tastego:
tastego/
main.go
common/
common.go
util/
util.go
其中除了 main.go 主函數以外,還包含2個自己寫的庫(package),一個是common,一個是util,那么為了讓項目可以正常編譯,這2個庫應該在GOPATH中,那么實際上在GOPATH的結構下,你的項目目錄是這樣的(wonderflow是我的github ID):
$GOPATH/
src/
github.com/
wonderflow/
tastego/
main.go
common/
common.go
util/
util.go
然后加入了vendor機制后,你的項目目錄下增加一個vendor的文件夾,里面可以放別的依賴,形式上就是:
$GOPATH/
src/
github.com/
wonderflow/
tastego/
main.go
common/
common.go
util/
util.go
vendor/
github.com/
qiniu/
...
golang.org/
...
讓我們再回顧一下本文剛開始描述的基于GOPATH的包管理方法:
tastego/
src/
github.com/
wonderflow/...
qiniu/...
golang.org/...
bin/
pkg/
看上去非常相似,只是有了vendor,就有了官方的正名!并且你再也不需要手工(半手工)修改GOPATH,項目的形態也跟以前的統一起來了,好處顯而易見。
但是問題就真的解決了嗎?實際上并沒有全部解決問題,反而由于在隨后2016、2017年,vendor機制成為正式的Go規則,問題日益嚴重。
- 嵌套的vendor目錄問題:vendor目錄下面的項目里面的vendor目錄怎么辦?
- vendor機制本身沒有版本概念,不同版本間類型不兼容問題依舊存在。
- 與其他 GOPATH 下的包init函數沖突問題:出現了相同的包,重復的init() 函數又怎么辦?
所以Golang團隊成員也召開了大會,非常贊同社區里各種包管理工具的理念,確實有必要對包管理提出一個統一的規則,來解決上面的問題。但是問題不是沒有規則,而是規則太多了。往往就是一個意見不合,一下子就殺出來一個新的工具。僅官方推薦的包管理工具就有15種之多。
- dep
- manul - Vendor packages using git submodules.
- Godep
- Govendor
- godm
- vexp
- gv
- gvt - Recursively retrieve and vendor packages.
- govend - Manage dependencies like go get but for /vendor.
- Glide - Manage packages like composer, npm, bundler, or other languages.
- Vendetta
- trash
- gsv
- gom
- Rubigo - Golang vendor utility and package manager
具體的可以參見官方的wiki頁面: https://github.com/golang/go/wiki/PackageManagementTools
所以Go官方也開始嘗試把包管理做成 go tools 工具鏈中的一個,官方的包管理工具就是 dep 但是目前這個項目還不成熟,還沒有納入到工具鏈中。
但是官方的建議已經很明顯了,讓大家盡量使用包管理工具去引入依賴,當然最好是盡量使用標準庫;另一方面則是盡量使用現有的包管理工具,而不是自己再去造一套規則。
所以,我們也來學習一下包管理工具該怎么用,經過多次對比調研,筆者推薦
Govendor 工具,所以也以之為例介紹。
Govendor
Govendor 本質上就是把源碼拷貝到vendor目錄下,通過在vendor目錄維護一個 vendor.json 的文件,指定使用的包版本。整個目錄結構清晰,在同步到github時,既可以把代碼直接全部包含到項目中,也可以用 .gitignore 忽略依賴的庫并通過 govendor sync 同步。
安裝
安裝就跟所有golang的工具一樣,go get 即可.
go get -u github.com/kardianos/govendor
初始化項目
對于一個現有的項目,沒有使用過任何包管理工具的話,開始使用Govendor 非常簡單。
進到項目目錄下,執行初始化:
cd "my project in GOPATH" govendor init
將現有依賴加入到當前項目的 vendor 中管理。
govendor add +external
此時,你已經順利將現有項目切換到govendor管理了。
項目過程中的常用命令
初始化項目完畢后,就到了項目常規管理階段,通常情況下,會有下列這些場景的需求。
添加依賴
如果你本地GOPATH中已經存在,使用 govendor add
# 指定版本的commit,包名后跟 @ 符號加上 commit ID govendor add golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55 # 指定版本名稱,包名后跟 @ 符號加上版本名稱或分支名稱 govendor add golang.org/x/net/context@v1 # Get latest v1.*.* tag or branch.
如果你本地不存在,使用 govendor fetch, 其他指定版本的方式與 govendor add 相同
一次性把所有項目的依賴庫全加載進來,就是我們初始化時介紹的命令。
govendor add +external
移除依賴
移除一個依賴
govendor remove golang.org/x/net/context
移除所有項目已經不用的依賴
govendor remove +unused
更新依賴
當然你可以選擇 govendor remove ,然后再 govendor add。
你還可以直接使用 govendor update 更新本地GOPATH中已經更新的包。
若本地不是最新的或不存在,請用 govendor fetch 更新。
govendor update golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55
同步govenodr包
一般情況下,開源項目的協作過程中,其他人更新了項目的govendor,那么你也要同步過來,直接使用下面的命令即可。
govendor sync
查看本地依賴以及包狀態
通常情況下拿到一個項目可能會想要直觀的了解他有哪些依賴關系,使用 govendor list即可查看。
純粹使用 govendor list 價值不大。
有意思的是,Govendor給Golang的依賴包加入了狀態描述,結合各類vendor的狀態參數進行各類操作就很有意思。
狀態一共有如下幾類:
+local (l) 僅存在項目中的包
+external (e) 在GOPATH中有,但是項目中沒有的包
+vendor (v) 在項目vendor目錄中的包
+std (s) 使用golang標準庫
+excluded (x) 項目中不包含且明確申明要排除在外的包
+unused (u) 在vendor目錄中,但實際項目沒有用到的包
+missing (m) 項目中用到但是找不到的包(此時需要govendor fetch獲取)
+program (p) 帶有main函數入口的包
+outside 所有外部包組合, 包括 (+external +missing)
+all 列出所有的包
這些狀態信息可以與其他命令連用,比如
govendor add +external
govendor remove +unused
最酷的是,狀態還可以做邏輯 與 以及 或 的操作,比如:
+local,program (local AND program) 表示項目中的包同時又是主函數入口 +local +vendor (local OR vendor) 表示項目中的包以及vendor中的包 +vendor,program +std ((vendor AND program) OR std) 表示vendor中的包同時帶有主函數入口,再加上標準庫的包 +vendor,^program (vendor AND NOT program) 表示vendor目錄中的包,但是不包含有主函數入口的包
查看包之間的依賴關系
使用 -v 參數可以查看一個包被哪些包依賴:
govendor list -v
那么反過來,你可能想知道一個包依賴了哪些包?這個是go工具鏈里面提供的方法,直接使用 go list
比如:
go list -f '{{ .Imports }}' github.com/wonderflow/tastego
查看包的實際路徑
通過 -p 參數可以看到包所在的實際文件路徑,區別于import時填寫的路徑,實際路徑可以快速找到你引用的包位置。
查看所有當前項目的包:
govendor list -p -no-status +local
至此,我們,Govendor的常用命令已經介紹完了,相信掌握了這些,在今后的項目中管理各種依賴包,你一定游刃有余。
via:http://www.infoq.com/cn/articles/history-go-package-management