Go 依赖管理
Golang包管理工具glide簡介
前言
Golang是一個十分有趣,簡潔而有力的開發語言,用來開發并發/并行程序是一件很愉快的事情。在這里我感受到了其中一些好處:
- 沒有少了許多代碼格式風格的爭論,強制統一的風格多好;
- 編譯速度超快,再也不用等待許久,才能編譯完工程(測試驅動開發自然更爽);
- 也不會出現同一個項目組中的人,在使用同一個語言的不同子集。但這種情況不論是在C#還是在Java的世界里,都還是普遍存在的;
- 輕松跨平臺(當然Java做得不錯,C#/.net還是努力中...)
- 這是一門非常簡潔、簡單、清晰的編程語言(關鍵字好少啊)
- 包依賴處理得很有趣;
- 錯誤處理機制很有趣,個人覺得比C#/Java的異常處理機制更方便,也更合理些;
- 對面向對象編程很有趣,非侵入性的接口實現方式,太贊了。
- 用組合而不是繼承
有些東西沒有,比如模板,這個東西有一大片爭論,官方也有明確的回應。是啊,為什么一定要模板呢?非要不可嗎?這么關鍵嗎?如果非要不可,是不是go并不是這個環境下的最佳選擇呢。
還有一個問題的是包管理,并沒有官方最佳管理方案,在go的世界里存在大量的自制解決方案。go語言的包是沒有中央庫統一管理的,通過使用go get命令從遠程代碼庫(github.com,goolge code 等)拉取,直接跳過中央版本庫的約束,讓代碼的拉取直接基于源代碼版本控制庫,開發者間的協同直接依賴于源代碼的版本控制。直接去除了庫版本的概念。沒有明顯的包版本標識,感覺還是有點不適應,官方的建議是把外部依賴的代碼全部復制到自己可控的源代碼庫中,進行同意管理。從而做到對依賴包的可控管理。
1.5版本的vendor目錄特性后,官方wiki推薦了多種支持這種特性的包管理工具如:Godep、gv、gvt、glide、Govendor等。我比較喜歡glide。此外,作為程序員FQ似乎是必備技能,翻得一手好墻,才能跟得上步伐,才有收起刀落的手感,怎一個爽字了得。別問我怎么FQ,找度娘,她知道。
glide
glide是Go的包管理工具。支持語義化版本,支持Git、Svn等,支持Go工具鏈,支持vendor目錄,支持從Godep、GB、GPM、Gom倒入,支持私有的Repos和Forks。
使用glide管理的工程目錄結構如下:
- $GOPATH/src/myProject (Your project)||-- glide.yaml||-- glide.lock||-- main.go (Your main go code can live here)||-- mySubpackage (You can create your own subpackages, too)| || |-- foo.go||-- vendor|-- github.com||-- Masterminds||-- ... etc.安裝
$ curl https://glide.sh/get | sh初始化
$ glide init初始化,glide掃描代碼目錄,創建一個glide.yaml文件,文件中記錄了所有的依賴
編輯配置
$ edit glide.yaml通過修改glide.yaml文件,可以添加版本信息等,這一步不是必須的。
解析下載包依賴
$ glide update或者
$ glide up下載和更新glide.yaml中列出的所有依賴包,并將它們放到vendor目錄下。glide同時也遞歸獲取依賴包需要的任何依賴項包括配置文件中定義的依賴項目。glide遞歸獲取依賴,可以識別Glide、Godep、gb、gom和GPM管理的項目。
當依賴被制定到特定的版本時,名為glide.lock的文件會被創建或者更新。例如,如果在glide.yaml中一個版本被指定在一個范圍內(如:^1.2.3),那么glide將在glide.yaml中設定一個特定提交ID(commit id)。如此,將允許重復安裝(見 glide install命令)。
從獲取的依賴包中移除嵌套的vendor/目錄可以使用-v標記。
安裝特定版本
當需要從glide.lock文件中安裝制定版本的包是,可以使用install命令:
glide install該命令將會讀取glide.lock文件,當glide.lock文件和glide.yaml不同步時,如glide.yaml發生改變,glide將會提供一個警告。運行glide up命令更新依賴樹時,將會重建glide.lock文件。
查看glide.yaml中依賴名稱
$ glide name查看依賴列表
$ glide list查看幫助
$ glide help參看glide版本信息
$ glide --version無論何種語言,依賴管理都是一個比較復雜的問題。而Go語言中的依賴管理機制目前還是讓人比較失望的。在1.6版本之前,官方只有把依賴放在GOPATH中,并沒有多版本管理機制;1.6版本(1.5版本是experimental feature)引入vendor機制,是包依賴管理對一次重要嘗試。他在Go生態系統中依然是一個熱門的爭論話題,還沒有想到完美的解決方案。
看其它
我們先來看看其它語言怎么解決,例舉兩種典型的管理方式:
Java
開發態,可以通過maven和gradle工具編輯依賴清單列表/腳本,指定依賴庫的位置/版本等信息,這些可以幫助你在合適的時間將項目固化到一個可隨時隨地重復編譯發布的狀態。這些工具對我來說已經足夠優雅有效。但maven中也有不同依賴庫的內部依賴版本沖突等令人心煩的問題。尤其是在大型項目中的依賴傳遞問題,若團隊成員對maven機制沒有足夠了解下,依賴scope的濫用,會讓整個項目工程的依賴樹變得特別的巨大而每次編譯效率低下。運行態,目前Java也沒有很好的依賴管理機制,雖有classloader可以做一定的隔離,但像OSGi那種嚴格的版本管理,會讓使用者陷入多版本相互沖突的泥潭。
Node.js
npm是Node.js的首選模塊依賴管理工具。npm通過一個當前目錄的 package.json 文件來描述模塊的依賴,在這個文件里你可以定義你的應用名稱( name )、應用描述( description )、關鍵字( keywords )、版本號( version )等。npm會下載當前項目依賴模塊到你項目中的一個叫做node_modules的文件夾內。與maven/gradle不同的是,maven最終會分析依賴樹,把相同的軟件默認扁平化取最高版本。而npm支持nested dependency tree。nested dependency tree是每個模塊依賴自己目錄下node_modules中的模塊,這樣能避免了依賴沖突, 但耗費了更多的空間和時間。由于Javascript是源碼發布,所以開發態與運行態的依賴都是基于npm,優先從自己的node_modules搜索依賴的模塊。
go get
Go對包管理一定有自己的理解。對于包的獲取,就是用go get命令從遠程代碼庫(GitHub, Bitbucket, Google Code, Launchpad)拉取。這樣做的好處是,直接跳過了包管理中央庫的的約束,讓代碼的拉取直接基于版本控制庫,大家的協作管理都是基于這個版本依賴庫來互動。細體會下,發現這種設計的好處是去掉冗余,直接復用最基本的代碼基礎設施。Golang這么干很大程度上減輕了開發者對包管理的復雜概念的理解負擔,設計的很巧妙。
當然,go get命令,仍然過于簡單。對于現實過程中的開發者來說,仍然有其痛苦的地方:
- 缺乏明確顯示的版本。團隊開發不同的項目容易導入不一樣的版本,每次都是get最新的代碼。尤其像我司對開源軟件管理非常嚴格,開源申請幾乎是無法實施。
- 第三方包沒有內容安全審計,獲取最新的代碼很容易引入代碼新的Bug,后續運行時出了Bug需要解決,也無法版本跟蹤管理。
- 依賴的完整性無法校驗,基于域名的package名稱,域名變化或子路徑變化,都會導致無法正常下載依賴。我們在使用過程,發現還是有不少間接依賴包的名稱已失效了(不存在,或又fork成新的項目,舊的已不存維護更新)。
而Go官方對于此類問題的建議是把外部依賴的代碼復制到你的?源碼庫中管理?。把第三方代碼引入自己的代碼庫仍然是一種折中的辦法,對于像我司的軟件開發流程來說,是不現實的:
- 開源掃描會掃描出是相似的代碼時,若License不是寬松的,則涉及到法律風險,若是寬松的,開源掃描認證確認工作也很繁瑣。
- 如何升級版本,代碼復制過來之后,源始的項目的代碼可以變化很大了,無明顯的版本校驗,借助工具或腳本來升級也會帶來工作量很大。
- 復制的那一份代碼已經開始變成私有,第三方代碼的Bug只能自己解決,難以貢獻代碼來修復Bug,或通過推動社區來解決。
- 普通的程序問題可能不是很大問題,最多就是編譯時的依賴。但如果你寫的是一個給其他人使用的lib庫,引入這個庫就會帶來麻煩了。你這個庫被多人引用,如何管理你這個庫的代碼依賴呢?
好在開源的力量就是大,Go官方沒有想清楚的版本管理問題,社區就會有人來解決,我們已經可以找到許多不錯的解決方案,不妨先參考下?官方建議?。
vendor機制
vendor是1.5引入為體驗,1.6中正式發布的依賴管理特性。Go團隊在推出vendor前已經在Golang-dev group上做了長時間的調研。最終Russ Cox在?Keith Rarick?的proposal的基礎上做了改良,形成了Go 1.5中的vendor:
- 不rewrite gopath
- go tool來解決
- go get兼容
- 可reproduce building process
并給出了vendor機制的”4行”詮釋:
If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import “p” is interpreted as import “d/vendor/p” if that exists.
When there are multiple possible resolutions,the most specific (longest) path wins.
The short form must always be used: no import path can contain “/vendor/” explicitly.
Import comments are ignored in vendored packages.
總結解釋起來:
- vendor是一個特殊的目錄,在應用的源碼目錄下,go doc工具會忽略它。
- vendor機制支持嵌套vendor,vendor中的第三方包中也可以包含vendor目錄。
- 若不同層次的vendor下存在相同的package,編譯查找路徑優先搜索當前pakcage下的vendor是否存在,若沒有再向parent pacakge下的vendor搜索(x/y/z作為parentpath輸入,搜索路徑:x/y/z/vendor/path->x/y/vendor/path->x/vendor/path->vendor/path)
- 在使用時不用理會vendor這個路徑的存在,該怎么import包就怎么import,不要出現import “d/vendor/p”的情況。vendor是由go tool隱式處理的。
- 不會校驗vendor中package的import path是否與canonical import路徑是否一致了。
vendor機制看似像node.js的node_modules,支持嵌套vendor,若一個工程中在著兩個版本的相的包,可以放在不同的層次的vendor下:
- 優點:可能解決不同的版本依賴沖突問題,不同的層次的vendor存放在不同的vendor。
- 缺點:由于go的package是以路徑組織的,在編譯時,不同層次的vendor中相同的包會編譯兩次,鏈接兩份,程序文件變大,運行期是執行不同的代碼邏輯。會導致一些問題,如果在package init中全局初始化,可能重復初化出問題,也可能初化為不同的變量(內存中不同),無法共享獲取。像之前我們遇到gprc類似的問題就是不同層次的相同package重復init導致的,見社區反饋?。
所以Russ Cox期望大家良好設計工程布局,作為lib的包?不攜帶vendor更佳 ,一個project內的所有vendor都集中在頂層vendor里面。
后續
Go的包依賴問題依舊困擾著開發人員,嵌套vendor可以一定程度解決多版本的依賴沖突問題,但也引入多份編譯導致的問題。目前社區也在一直討論如何更好的解決,將進入下一個改進周期。這次將在Peter Bourgon的主持下正式啟動:?go packaging proposal process?,當前1.8版本特性已凍結,不知這個改進是否會引入到1.9版本中。
參考:
[1]?理解Go 1.5 vendor
[2]?Golang的包管理之道
Go 語言本身提供的包管理機制
在 Go 語言中,我們可以使用go get命令安裝遠程倉庫中托管的代碼,不同于 Ruby Gem、pypi 等集中式的包管理機制, Go 語言的包管理系統是去中心化的。簡單來講,go get命令支持任何一個位置托管的 Git 或 Mercurial 的倉庫,無論是 Github 還是 Google Code 上的包,都可以通過這個命令安裝。
我們知道,在 Go 語言中的import語句對于已經使用go get安裝到本地的包,依然要使用其去絕對路徑引入。 比如對于從 Github 上安裝的?goji,其在 Github 上的路徑 URL 是https://github.com/zenazn/goji,因此在import它的時候需要使用下面的代碼:
| 1 | import "github.com/zenazn/goji" |
正因為如此,Go 語言可以通過直接分析代碼中的import語句來查詢依賴關系。?go get命令在執行時,就會自動解析import來安裝所有的依賴。
除了go get,Go 語言還提供了一個 Workspace 的機制,這個機制也是很容易讓人困惑的設計。簡單來說就是通過設定?GOPATH環境變量,指定除了GOROOT所指定的目錄之外,Go 代碼所在的位置(也就是 Workspace 的位置)。 一般來說,GOPATH目錄下會包含pkg、src和bin三個子目錄,這三個目錄各有用處。
- bin?目錄用來放置編譯好的可執行文件,為了使得這里的可執行文件可以方便的運行, 在 shell 中設置PATH變量。
- src?目錄用來放置代碼源文件,在進行import時,是使用這個位置作為根目錄的。自己編寫的代碼也應該放在這下面。
- pkg?用來放置安裝的包的鏈接對象(Object)的。這個概念有點類似于鏈接庫,Go 會將編譯出的可連接庫放在這里, 方便編譯時鏈接。不同的系統和處理器架構的對象會在pkg存放在不同的文件夾中。
我的GOPATH目錄樹如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | ├── bin ├── pkg │?? └── darwin_amd64 │?? └── github.com │?? └── zenazn │? ?? └── goji └── src ├── code.google.com │?? └── p │?? └── go.crypto └── github.com ?? └── zenazn ?? └── goji |
一般來說,你自己的代碼不應該直接放置在src目錄下,而應該為其建立對應的項目文件夾。?go get也會把第三方包的源代碼放到這個目錄下,因此一般推薦設置兩個GOPATH,比如:
| 1 | export GOPATH="/usr/local/share/go:$HOME/codes/go" |
這樣第三方包就會默認放置在第一個路徑中,而你可以在第二個路徑下編寫自己的代碼。 雖然 Go 語言本身已經提供了相當強大的包管理方式了,但是仍然有一些不足:
因此我們還需要一些第三方的工具來彌補這些缺陷。
第三方的管理工具
GOPATH 管理和包管理
由于存在GOPATH的機制,我們可以使用多個GOPATH來實現項目隔離的方法。 譬如,對于每個項目,都分配一個不同的路徑作為GOPATH。 可以實現這樣的目的的工具有gvp等。
對于 gvp 來說,想要針對當前目錄建立一個GOPATH,只需要執行gvp init即可。 gvp 會在當前項目的目錄下新建一個隱藏的文件夾作為GOPATH指向的位置。 切換環境時使用下面兩個命令來修改環境變量。這種做法跟 Python 中的virtualenv比較類似。
| 1 2 | source gvp in # 進入當前目錄對應的 GOPATH 環境 source gvp out # 登出當前目錄對應的 GOPATH 環境 |
至于對依賴包更版本更細致的管理,可以配合的工具還有?gpm。?gpm有點類似于 Python 中的pip工具。他可以生成一個名為?Godeps?的文件, 其中記錄了每個依賴包的 URL 以及使用的版本(hash tag)。 之前的一篇文章提到?gpm只能管理來自 Github 的依賴,不過當前的版本已經支持了非 Git 方式托管的依賴包了。
基于同樣原理管理依賴包版本的工具還有Godep。 這個工具在 Github 上具有相當高的關注度。它所生成的Godeps文件采用 JSON 格式儲存, 是一個跟 Node.js 中?NPM?相仿的工具。
總體來說以上幾個工具已經可以解決隔離項目環境和控制依賴包版本的問題了。但是使用上還不算方便, 為了能在我們 cd 到某個目錄時自動的切換環境變量,我們可能還需要在 shell 做一些配置使其在cd到項目目錄下時自動切換環境變量。
這方面做的比較好的一個選擇是?Go Manager(gom), 它生成的Gomfile格式上幾乎跟 Ruby Gem 一樣。gom 可能是這些工具當中使用最方便的一個, 只要使用gom build命令代替原來的go build命令進行編譯,你基本不需要配置 Shell 或者和環境變量打交道。
Go 語言版本管理
對于 Go 語言,一般來說并沒有使多個語言版本并存的需求。Go 語言現在還沒有經歷過類似 Python 2.x 到 3.x 或者 Ruby 1.x 到 2.x 這樣破壞性的版本升級。舊的代碼在新的語言版本當中一般是能夠正確運行的。 不過若遇到非要并存多個版本的時候,gvm就是一個不錯的選擇。
gvm 的使用跟?rvm?比較類似。
| 1 2 | gvm install go1 # 安裝 go1 版本 gvm use go1 # 修改環境變量使用 go1 版本的 Go |
總結
是否有必要使用多個 Workspace 仍然具有爭議,譬如這個 StackOverflow 上的相關問答中, 就有人提出只使用一個 Workspace 就可以應付大多數情況了。
在研究相關問題的時候,我發現很多 Go 語言的用戶都還帶著原來編程語言的思維, 這點從上面介紹的多個工具的特點當中就可以很容易看出來:gvp和gpm就是典型的 Python 的包管理模式,?gvp對應著virtualenv,gpm對應著pip;如果你之前是 Node.js 和 NPM 的用戶, 那么GoDeps肯定會讓你有種熟悉的感覺;更不用說最后介紹的gom了,它從名稱到文件格式都在模仿 Ruby Gem。
不同編程背景的開發者來到 Go 語言之后各自帶來了自己的依賴包管理方式,而且形成了各自的社區。 這種現象雖然使得各自圈子的開發者免去了選擇恐懼癥,但是造成的解決方案分裂和互不兼容的情況也需要正視。 這時我們不禁要問,Go 自己的解決方式應該是什么樣的?Go 語言為何沒有一個官方標準的解決方案呢?
從Go FAQ的一段文字當中我們可以得到部分答案:
Versioning is a source of significant complexity, especially in large code bases, and we are unaware of any approach that works well at scale in a large enough variety of situations to be appropriate to force on all Go users. (依賴包的版本管理是一個非常復雜的問題,特別是在代碼量比較大的時候。 我們一直沒有找到任何一種方式能夠在各種情形下都能良好工作, 因此也沒有一種方式足夠好到應該強迫所有的 Go 用戶使用它)
因此現階段來看,對于 Go 語言的包管理解決方案,我們也就只能“仁者見仁,智者見智”了。
最后,對于想要了解 Go 語言的包管理以及更多可用的工具的讀者,這里再推薦兩篇相關的文章:?Go Package Management?和?A Journey in Golang Package Manager
總結
- 上一篇: 深入浅出Go Runtime
- 下一篇: ✿ iBm T60 水货入手了 满意 2