傳統Go構建以及包依賴管理
- Go在構建設計方面深受Google內部開發實踐的影響,比如go get的設計就深受 Google內部單一代碼倉庫(single monorepo)和基於主幹(trunk/mainline based)的開發模型 的影響:只擷取Trunk/mainline代碼和版本無感知
image.png
- 我們知道go get擷取的代碼會放在GOROOT/src下面,而go build會在GOROOT/src和GOPATH/src下面按照import path去搜尋package,由於go get 擷取的都是各個package repo的trunk/mainline的代碼,因此,Go 1.5之前的Go compiler都是基於目標Go程式依賴包的trunk/mainline代碼去編譯的。這樣的機制帶來的問題是顯而易見的,至少包括:
- 因依賴包的trunk的變化,導致不同人擷取和編譯你的包/程式時得到的結果實質是不同的,即不能實現reproduceable build
- 因依賴包的trunk的變化,引入不相容的實現,導致你的包/程式無法通過編譯
- 因依賴包演化而無法通過編譯,導致你的包/程式無法通過編譯
- 為了實現reporduceable build,Go 1.5引入了Vendor機制,Go編譯器會優先在vendor下搜尋依賴的第三方包,這樣如果開發人員將特定版本的依賴包存放在vendor下面並提交到code repo,那麼所有人理論上都會得到同樣的編譯結果,從而實現reporduceable build
Go語言版本控制
在沒有版本控制時,go get 的一個重大缺陷是對於給定的更新無法知道是否是使用者所期望的。
Go 將在下個版本(1.11)中看到官方的包版本控制,去除了 GOPATH 依賴,同時還引入了 module(模組) 的概念。
版本控制可以讓我們能夠實現重編譯。當我讓你試用我程式最新版本時,我清楚的知道你不僅僅擷取到的是我最新程式的代碼,還包括我代碼所依賴的相同版本的包,這樣才能編譯出完全一樣的二進位包。
版本控制還能讓我們不同階段保持同樣的編譯方式,即使我們的依賴包可能有新版本了,只要我們的配置未允許使用,go 命令也不會使用新版本的包。
儘管添加版本控制是必須的功能,但同時也不能失去go 命令列現有的優秀特性:簡單、高效、易懂,所以它應該足夠透明不能破壞掉go get 本身功能。
Go新版本中保留了go get的精華部分,增加了重複構建,採用了語義化的版本控制,棄用了 vendor,廢棄了基礎工程建立時依賴GOPATH,並且提供了老項目平滑遷移的方式。
Go添加版本控制共分四個步驟
匯入相容規則
包管理系統中最大的痛苦在於解決相容性問題
比如,大多數系統中包B 聲明需要的包D 版本是6或者更高版本,然後包C聲明所需的包D 版本是2,3和4,但不能高於版本5。如果你正在編寫包A ,你想同時引入包B 和C ,那麼你不走運了:沒有一個獨立的D 版本可以供B 和C 同時選擇編譯進A。B 和C 做的都是合理的,你也沒辦法改變它,所以你就被卡住了。
為了避免主導者設計一個導致現有的大型程式無法編譯的系統,提案要求包作者遵循以下匯入相容性原則:
如果一箇舊包和新包有相同的匯入路徑,新包必須向後相容舊包 這條規則是對前面 Go FAQ 的重申,引用 FAQ 中最後講的:“如果需要完全變更,那麼就建立個新匯入路徑的包”。開發人員希望能通過語義化的版本來表達這樣一個變更,因此我們把語意化版本控制系統也加入到我們提案中。具體點說,主要版本2 和更新的版本可以通過在路徑中包含版本資訊來區分,比如:
import "github.com/go-yaml/yaml/v2"
- 包作者遵循匯入相容性原則可以讓我們減少適配工作,讓系統更簡單的同時也讓包生態減少片段化
當然,實際上儘管作者盡最大努力去做了,更新時也難免會出現破壞使用者使用的情況。因此,使用一個不頻繁升級的升級機制很重要,這也是接下來我們要講的。
最小版本規則
幾乎現在所有的包管理組件括dep和cargo都在構建時使用最新的包版本,基於兩方面的重要因素,被認為這是個錯誤的約定:
首先,“最新可用版本”有可能因為外來事件導致變更,像新版本發布。也許今晚你依賴的包中有人會發布個新版本,第二天早上你再編譯有可能就產生不同的結果了;
第二,為了覆蓋這個預設約定,開發人員花費大量的時間告訴包管理器不使用哪個版本的包。
提案中我們使用了不同的方式,稱之為最小版本選擇。
構建時每個包預設使用的是最老的可用版本,這個方式讓昨天和今天的編譯不會有變化,因為你總不會在今天發布一個更老版本吧。更好的是,開發人員只需告訴包管理器最小可用的那個版本,包管理器就可以很快的決定哪個版本可用。我們稱它為最小版本選擇一方面是因為我們選擇的是最小版本,另一方面是因為對整個系統來說是最小化的,避免了現有系統的複雜性。
最小版本選擇為模組指定了其相依模組的最低版本需求,這為後續升級和降級操作提供了一個很好的選擇。同時,它還可以通過排除指定版本的依賴或者指定特殊版本依賴完成編譯。
最小版本選擇在不鎖定檔案情況下預設就完成了可重複構建。
最小版本選擇是匯入相容的關鍵。使用者不會再說:“不,版本太新了”,更多情況是面臨“不,版本太舊了”,這種情況下解決方案很明確:升級新版本就可以了。
Go Module
Go Module是共用一個匯入路徑首碼的包集合,也就是我們所說的模組路徑
Module是版本控制的單元,Module的版本通過語義化的版本字串表示,當開發中使用Git 時,開發人員通過給模組的Git 資產庫添加一個新tag的方式來定義一個新的語義化版本。儘管強烈推薦使用語義化版本的方式,但也支援指向特定commit。
模組定義在一個叫go.mod的新檔案裡,裡麵包含了模組所依賴包的最小版本
下面就是個簡單的go.mod檔案:
module "rsc.io/hello"require ("golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54"rsc.io/quote" v1.5.2)
這個檔案通過路徑標識 rsc.io/hello 定義了一個模組,它本身還依賴於兩個其他模組:golang.org/x/text 和 rsc.io/quote ,這個模組自身編譯的時候使用的是 go.mod 檔案中指定的依賴列表的版本。對於更上一層的編譯,其他匯入這個模組的地方將使用它較新的版本編譯。
包發行者最好使用語義化的 tag 發布版本,vgo 也鼓勵通過打tag的版本號碼方式,而不是任意的提交版本。
除了指定必須的依賴版本,go.mod 檔案還可以實現前面章節中提到的排除和替換的版本,但是這些只有當直接編譯該模組的時候起作用,在模組作為整體工程一部分編譯時間就不行了
Goinstall 和舊的 go get 通過像git 和hg 這樣的版本控制工具直接下載代碼,這種方式存在很多問題,其中包括片段化嚴重:使用者如果沒有bzr 就沒法下載託管在Bazaar 資產庫的代碼。相比之下,Go Module則是通過HTTP 下載zip 包的方式。
Module統一通過zip包的形式提供可以讓下載協議更簡單,公司或者個人可以處於任何原因考慮(安全或者想要快取複本防止源被刪除)自己做下載代理,使用代理來確保可用性並且通過go.mod定義了哪些代碼需要用到
Go 命令
go 命令必須更新才能使用模組功能。一個重要的變化就是常用的構建命令,像 gobuild, go install, go run, 和 go test 將需要按指定需求解析對應的依賴關係了
最重要的變化還是終結了GOPATH作為Go 代碼工作空間的設定,由於go.mod檔案包含了完整的模組路徑並且還定義了每個使用的依賴的版本,因此包含go.mod檔案的目錄就可以被認為是一個分類樹的根目錄了,該分類樹作用於自身的工作空間,並且和其他類似的目錄彼此隔離。現在你只需git clone然後cd就可以直接擼代碼了,不再需要GOPATH了