轉載自:https://io-meter.com/2014/07/30/go's-package-management/對于從 Ruby、Python 或者 Node 等編程語言轉向 Go 語言的開發者,可能會有一個疑問: Go 語言中的包依賴關系是怎么管理的?有沒有什么方便使用的工具呢? 我最近研究了一下這個問題,以下是我的研究報告。
(圖片來源:nathany.com)
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 語言本身已經提供了相當強大的包管理方式了,但是仍然有一些不足:
- 不能很方便地隔離不同項目的環境
- 不能很方便地控制某個依賴包的版本
- 不能管理 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