這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
對於從 Ruby、Python 或者 Node 等程式設計語言轉向 Go 語言的開發人員,可能會有一個疑問:Go 語言中的包依賴關係是怎麼管理的?有沒有什麼方便使用的工具呢?我最近研究了一下這個問題,以下是我的研究報告。
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
|
├── 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 varietyof situations to be appropriate to force on all Go users.(依賴包的版本管理是一個非常複雜的問題,特別是在代碼量比較大的時候。我們一直沒有找到任何一種方式能夠在各種情形下都能良好工作,因此也沒有一種方式足夠好到應該強迫所有的 Go 使用者使用它)
因此現階段來看,對於 Go 語言的包管理解決方案,我們也就只能“仁者見仁,智者見智”了。
最後,對於想要瞭解 Go 語言的包管理以及更多可用的工具的讀者,這裡再推薦兩篇相關的文章:Go Package Management 和A Journey in Golang Package Manager