也許 Go 開發能夠更簡單!Go += Package Versioning

簡評:對於Go來講一直以來依賴包的版本控制上沒有一個好的方案,儘管社區誕生了不下十餘個解決該問題的工具,但一直以來沒有一個官方的支持。這個提案有望在Go
的下個版本中看到官方的包版本控制了,去除了GOPATH依賴,同時還引入了module 的概念,真正意義上實現了重編譯,可謂一次大的變動

編譯:繆斯的情人html

是時候爲 Go 添加包版本控制了!git

更確切地說,咱們須要把包版本的概念普及到Go 開發者和工具經常使用詞彙中,以便在後續的相互溝通時能準確一致的表達哪些程序代碼須要編譯、運行和解析。一樣,go 命令也須要準確的告訴開發者在編譯中使用了哪一個包的哪一個版本。程序員

版本控制可讓咱們可以實現重編譯。當我讓你試用我程序最新版本時,我清楚的知道你不只僅獲取到的是我最新程序的代碼,還包括我代碼所依賴的相同版本的包,這樣才能編譯出徹底同樣的二進制包。github

版本控制還能讓咱們不一樣階段保持一樣的編譯方式,即便咱們的依賴包可能有新版本了,只要咱們的配置未容許使用,go 命令也不會使用新版本的包。golang

儘管添加版本控制是必須的功能,但同時咱們也不能失去go 命令行現有的優秀特性:簡單、高效、易懂。目前來看,不少程序員仍是不過重視版本控制,主要是大部分狀況下也都沒出現什麼問題。試想若是咱們有一個合理模式的設計和默認配置,讓開發者不須要太多關注版本控制的東西,程序仍然能很好的工做,而且對現有工程影響較小,發佈新版本足夠簡單,甚至平常開發中能夠忽略掉版本控制的工做,這樣的版本控制模式纔是咱們想要的。算法

簡而言之,版本控制是必須的,可是應該足夠透明不能破壞掉go get 自己功能。這篇文章探討了一個能徹底實現這些的提案,而且提供了一個如今可用的原型demo,但願這能爲集成到go命令奠基基礎。我打算經過這篇文章討論下在產品化過程當中哪些是須要作的,哪些是不須要作的,基於這個討論,我將對提案和原型作進一步調整,而且提交一個官方提案,做爲可選功能集成到Go 1.11版本中。緩存

這個提案保留了go get的精華部分,增長了重複構建,採用了語義化的版本控制,棄用了vendor,廢棄了基礎工程建立時依賴GOPATH,而且提供了老項目平滑遷移的方式,目前這個提案還處於初級階段,若是細節上有問題,咱們會在Go主版本發佈前修復掉安全

背景

在咱們討論這個提案以前,先了解下當下的現狀。講起來可能會很長,但歷史的教訓對如今有很重要的參考意義,而且讓咱們可以清楚的知道這個提案改變了什麼。若是你以爲沒意思,那麼能夠直接跳到提案,或者去看原型demoide

Makefiles, goinstall, 和 go get

2009年11月,Go 發佈了帶有編譯器、連接器和一些內置庫的初代版本,在當時你必須經過運行6g6l 來編譯和連接你的程序,還包含一些簡單的makefiles。大多數狀況下,經過簡單包裝的gobuild能夠編譯一個單包程序而且生成對應的makefile。在當時,也沒有一個合適的方式把代碼分享給其餘人。儘管還有不少功能沒提供,可是仍然發佈了,而且Go 計劃將一些剩餘的功能放到社區去作。工具

2010年2月,goinstall 出現了,一個新的零配置的命令行,主要用來從源碼管理庫(像 Bitbucket 和 GitHub)下載packages。Goinstall引入了今天在Go開發者中已經普及的路徑約定,由於在當時沒有代碼遵循這個約定,goinstall 起初只能用在標準庫的導入上,可是開發者很快就把他們本身的命名約定遷移到今天咱們所知的統一約定上,這些發佈的Go packages 逐漸造成了一個連貫的生態系統。

Goinstall 同時還棄用了makefiles,消除了用戶在構建配置上的複雜性。儘管對 package 做者來講每次構建時不能生成代碼偶爾會以爲不方便,可是對於 package 使用者來講這個簡化卻很是重要:使用者沒必要擔憂安裝的 package 裏工具集編譯時和 package 做者編譯時不一致了,簡化對於一個工具來講頗有意義。對於分步編譯 package 來講 makefile 有必定的必要性;逆向工程中如何讓同一個 package 使用不一樣的工具(像go vet或者代碼完成)makefile在這方面就很難去作了。即使正確的維護好了編譯依賴關係,對於任何一個makefiles來講在必要時再進行重編譯也很困難。儘管一些人認爲去掉makefiles失去了靈活性,可是回頭來看,所得到的好處遠大於這些不便。

2011年12月,做爲Go 1 的預發版本的一部分,咱們介紹了go命令中使用 go get 來替換goinstall

總的來講,go get 是變革性的,它讓 Go 開發者可以共享代碼和相互構建,而且經過工具隔離了 go 命令編譯系統中的細節,可是,go get 缺乏版本控制的概念,實際上在goinstall第一次討論中就清楚的意識到須要版本控制相關的功能。不幸的是,至少在當時咱們的 Go 團隊中還不清楚應該怎麼去作。當go get須要一個包時,老是從像 Git 或者 Mercurial 這樣的遠程版本控制系統中下載最新的副本,包版本管理上的缺失至少致使了兩個重大的缺陷。

Versioning 和 API 的穩定性

首先在沒有版本控制時go get 的一個重大缺陷是對於給定的更新沒法知道是不是用戶所指望的。

在2013年11月,Go 1.2 添加了一個發關於包版本控制基本建議的FAQ:

公共發佈的包應該保持向後兼容性。 Go 1 compatibility guidelines
提供了一個好的參考:不要刪除導出的命名,鼓勵使用符合語義(composite
literals)tag命名版本等等。若是須要不一樣的功能請添加新的命名而不是修改舊的命名,若是須要徹底獨立的功能,在新的導入路徑下建立新
package

2014年3月,Gustavo Niemeyer 建立了gopkg.in,倡導「Go語言穩定性API」。這個網站實際上是個感知 GitHub 版本變化的重定向器,你能夠經過gopkg.in/yaml.v1gopkg.in/yaml.v2 的這樣的導入方式來指向Git庫的不一樣提交版本(也可能在不一樣分支)。按照這個思路,依賴庫的功能有了重大改變後你能夠把以前的 v1 版本導入路徑做爲備用,而後建立一個新版本 v2,經過v2 導入路徑引入徹底不一樣的API。

2015年8月,Dave Cheney 提出了語義化版本管理的提案,在接下來幾個月裏引起了一場有趣的討論,每一個人都認爲語義化版本管理是個好主意,可是沒有人知道下一步怎麼作:語義化版本應該使用什麼樣的工具呢?

任何關於語義化版本控制的的討論都不可避免的會有人用海勒姆法則的反駁:

當一個 API 有足夠的用戶的時候,在約定中你承諾的什麼都無所謂,全部在你係統裏面被觀察到的行爲都會被一些用戶直接依賴。
雖然海勒姆法則在經驗上來說是正確的,可是語義化版本控制仍然是不一樣發佈版本間創建一個合理指望關係的有效方式。通常來講,從1.2.3 升級到1.2.4 不該該破壞你的原有代碼,然而從1.2.3 升級到2.0.2 有可能會破壞原來的代碼。若是你的版本升級到1.2.4 出現了問題,那麼做者通常會根據 bug 的報告在1.2.5 修復掉,若是你的代碼升級到2.0.0 出現了問題,那麼有多是一次大升級的故意爲之。

Vendoring 和 可重複構建

go get 沒有版本控制概念的第二個重大缺陷是你頗有可能沒法實現重編譯的想法。你沒有辦法確認你的程序的使用者編譯的時候和你的編譯時依賴於相同版本的包,在2013年11月,Go 1.2 FAQ 中也增長了如下基礎建議:

若是你使用的是外部提供的包擔憂它會發生意想不到的改變時,最簡單的方式是拷貝到你本地的庫中(這是Google內部採用的方法)把它標記爲本地庫放到一個新的導入路徑下,好比把 "http://original.com/pkg"拷貝到 "http://you.com/external/original.com/pkg"。Keith
Rarick的goven 是一個自動化實現該功能的工具

Goven 是 Keith Rarick 在2012年3月發佈的,它將你依賴的包都拷貝到你本地資源庫而且更新全部的導入路徑指向新的本地路徑,用這種方式修改源代碼的依賴對於編譯來講是有效的,但也存在一些問題,這種修改讓本地的包很難和新的副本比較變化來合併須要的更新。

在2013年9月,Keith 發佈了godep,「一個凍結包依賴的新工具」。godep最重要的提高是添加了咱們如今都知道的Go vendoring ——無需修改源文件將依賴拷貝到項目中——無需工具鏈支撐,經過某種方式設置GOPATH實現

2014年10月,Keith 建議在Go 工具鏈中增長「外部包」的概念支持,以便於工具能夠根據約定更好的分析工程。在那個時候,也出現了不少相似於godep的工具,Matt Farina 寫了一篇文章「Glide in the Sea of Go Package Managers」 比較了godep和不少後來出現的工具,其中最爲突出的是glide

2015年4月,Dave Cheney 介紹了gb,一個「基於工程的構建工具...經過vendor中的源碼實現可重複構建」,並且無需從新導入,(gb的另外一個目的是避免將須要的代碼放在特殊指定的GOPATH目錄下,這對不少開發者來講都不是一個好的開發流程)。

那年春天,Jason Buberel 對Go 包管理工具進行了調查以便了解經過什麼樣的方式來整合下這些不一樣人努力的成果,避免出現重複和浪費的工做。他的調查讓 Go 團隊清楚的認識到go命令須要在包不從新導入狀況下直接支持vendoring。與此同時,Daniel Theophanes 開始制定了一個文件格式規範來描述vendor目錄下代碼的準確源和版本信息。在2015年6月,咱們接受了Keith 的提案vendor做爲Go 1.5 實驗特性,在Go 1.5 中是可選的,在Go 1.6 中是默認的特性。咱們鼓勵全部vendoring工具的做者和Daniel 一塊兒努力制定一個統一元數據格式的文件規範。

vendoring概念融入到Go 的工具鏈中可讓像go vet這樣的程序分析工具更好的理解工程。到如今爲止已經有數十個Go 包管理工具或者vendoring工具來管理 vendor 目錄。從另外一方面來講,由於這些工具使用了不一樣元數據的文件格式規範,他們之間也沒法輕易的共享依賴的相關信息。

更重要的是,vendoring並非一個完整的解決方案,它只是提供了可重編譯的實現而沒有解決包版本控制的問題,它並無幫助工程去理解包的版原本決定使用哪一個版本的包。像glidedep這樣的包管理器是經過對vendor目錄的某種設置隱式的將版本控制的概念引入到Go編譯中,也不須要直接的工具鏈支撐。事實上,在Go 生態系統中不少工具都不能識別版本,很顯然,Go須要對包版本提供直接的工具鏈支持。

一次官方包管理的實驗

在2016年GopherCon上,一羣對Go 感興趣的gophers 彙集在Hack Day 上圍繞Go包管理進行了一場討論,其中一個成果是成立一個委員會和包管理諮詢小組,目標就是建立一個新的Go 包管理工具,願景是統一現有的一些工具,實現方式上仍然經過vendor 目錄的方式。由Peter Bourgon 發起以及委員會成員Andrew Gerrand, Ed Muller, Jessie Frazelle, and Sam Boyer 共同起草了一份規範,在Sam 主導下實現了dep,相關背景信息請查看Sam 在2016年2月發佈的文章「So you want to write a package manager」 和2016年12月的文章 「The Saga of Go Dependency Management」 以及2017年7月GopherCon 的分享「The New Era of Go Package Management」。

Dep 有多種用途:它是基於當前最佳實踐的一次改進,也是邁向成功解決方案的重要一步,同時也是一次「官方實驗」,這幫助咱們更加深刻的瞭解到對於Go 開發者來講咱們哪些該作以及哪些沒有作好。可是dep並非go 命令集成包版本控制的最終原型。它經過一種強大靈活性的方式探索設計的空間,能夠說在咱們編譯 Go 程序時扮演着makefiles的功能,一旦咱們深刻的理解了它的設計思路以及聚焦到幾個必須支持的關鍵功能的時候,你會發現這樣的設計將會移除掉Go 生態中不少其餘功能下降了表達力成本,同時,強制的約定讓Go 代碼看起來更統一和易懂,對於構建工具來講也變得更簡單了。

咱們接下來看下dep下一階段的目標:完成go 命令集成最終原型的初稿,相似於goinstall 的包管理,這個原型是一個獨立的命令咱們稱之爲vgo,你能夠認爲它就是個支持包版本控制的go 命令。這是一個新的實驗特性,和當時引入goinstall 同樣,一些代碼和工程已經支持vgo了,其餘一些工程須要作些適配。跟移除 makefiles 時同樣去掉了一些控制和表現層相關的東西,簡化系統下降用戶使用成本。

vgo 的實驗並不意味着咱們將中止對dep的支持,咱們會持續保證dep的可用直到go 命令集成的路徑肯定、實現而且可用。固然咱們也儘量的保證能從dep平滑過渡到go 命令集成的方式,若是工程沒有使用dep(注意godep和glide已經中止更新,建議遷移到dep)那麼能夠直接遷移到vgo

提案

提案中關於在 go 命令中添加版本控制共分四個步驟。首先是要兼容Go FAQ 和gopkg.in 中的導入規則,也就是說創建一個預期,新版本的包導入路徑應該向後兼容於老版本。第二,採用一個簡單的新算法(稱爲最小版本選擇)來篩選出哪一個包版本在編譯時使用。第三,引入Go 模塊的概念,Go 模塊是一組包含單個版本的包而且聲明瞭它們所需的最低版本的依賴。第四,定義如何將這些改變集成到go 命令,所以從如今開始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"

建立了v2.0.0 版本,在語義化版本控制中意味着一次重大變動,按照導入兼容原則要建立一個新導入路徑的包。因爲每一個主要版本有不一樣的導入路徑,所以給定的Go 可執行程序中可能包含主版本中任意一個,這正是咱們預期想要的。

包做者遵循導入兼容性原則可讓咱們減小適配工做,讓系統更簡單的同時也讓包生態減小碎片化。固然,實際上儘管做者盡最大努力去作了,更新時也不免會出現破壞用戶使用的狀況。所以,使用一個不頻繁升級的升級機制很重要,這也是接下來咱們要講的。

最小版本選擇

幾乎如今全部的包管理包括depcargo都在構建時使用最新的包版本,基於兩方面的重要因素,我認爲這是個錯誤的約定。首先,「最新可用版本」有可能由於外部事件致使變動,像新版本發佈。也許今晚你依賴的包中有人會發布個新版本,次日早上你再編譯有可能就產生不一樣的結果了。第二,爲了覆蓋這個默認約定,開發者花費大量的時間告訴包管理器不使用哪一個版本的包。

提案中咱們使用了不一樣的方式,稱之爲最小版本選擇。構建時每一個包默認使用的是最老的可用版本,這個方式讓昨天和今天的編譯不會有變化,由於你總不會在今天發佈一個更老版本吧。更好的是,開發者只需告訴包管理器最小可用的那個版本,包管理器就能夠很快的決定哪一個版本可用。咱們稱它爲最小版本選擇一方面是由於咱們選擇的是最小版本,另外一方面是由於對整個系統來講是最小化的,避免了現有系統的複雜性。

最小版本選擇爲模塊指定了其依賴模塊的最低版本需求,這爲後續升級和降級操做提供了一個很好的選擇。同時,它還能夠經過排除指定版本的依賴或者指定特殊版本依賴完成編譯。

最小版本選擇在不鎖定文件狀況下默認就完成了可重複構建。

最小版本選擇是導入兼容的關鍵。用戶不會再說:「不,版本太新了」,更多狀況是面臨「不,版本太舊了」,這種狀況下解決方案很明確:升級新版本就能夠了。

Go Modules

Go 模塊是共享一個導入路徑前綴的包集合,也就是咱們所說的模塊路徑。模塊是版本控制的單元,模塊的版本經過語義化的版本字符串表示,當開發中使用Git 時,開發者經過給模塊的Git 資源庫添加一個新tag的方式來定義一個新的語義化版本。儘管強烈推薦使用語義化版本的方式,但也支持指向特定commit。

模塊定義在一個叫go.mod的新文件裏,裏面包含了模塊所依賴包的最小版本。下面就是個簡單的go.mod文件:

// My hello, world.

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/textrsc.io/quote ,這個模塊自身編譯的時候使用的是 go.mod 文件中指定的依賴列表的版本。對於更上一層的編譯,其餘導入這個模塊的地方將使用它較新的版本編譯。

包發佈者最好使用語義化的 tag 發佈版本,vgo 也鼓勵經過打tag的版本號方式,而不是任意的提交版本。 rsc.io/quote 模塊使用的是 tag 版本的方式,而 golang.org/x/text 模塊沒有提供一個tag版本。對於未命名的提交,v0.0.0-yyyymmddhhmmss-commit 表示一個指定日期的提交,在語義化版本控制中,字符串v0.0.0表示預發版本號,yyyymmddhhmmss-commit 表示預發版本的標識符。

除了指定必須的依賴版本,go.mod 文件還能夠實現前面章節中提到的排除和替換的版本,可是這些只有當直接編譯該模塊的時候起做用,在模塊做爲總體工程一部分編譯時就不行了,詳細可查看這個例子(examples)。

Goinstall 和舊的 go get 經過像git 和hg 這樣的版本控制工具直接下載代碼,這種方式存在不少問題,其中包括碎片化嚴重:用戶若是沒有bzr 就無法下載託管在Bazaar 資源庫的代碼。相比之下,模塊則是經過HTTP 下載zip 包的方式。以前,遇到特殊需求的包 go get 經過版本控制的命令行工具去主流的代碼託管網站下載,如今vgo 直接經過網站提供的 API 下載須要的包。

模塊統一經過zip包的形式提供可讓下載協議更簡單,公司或者我的能夠處於任何緣由考慮(安全或者想要緩存副本防止源被刪除)本身作下載代理,使用代理來確保可用性而且經過go.mod定義了哪些代碼須要用到,vendor 目錄也就再也不須要了。

go 命令

go 命令必須更新才能使用模塊功能。一個重要的變化就是經常使用的構建命令,像 gobuild, go install, go run, 和 go test 將須要按指定需求解析對應的依賴關係了,在新模塊中使用golang.org/x/text 時只需在Go 源碼中導入編譯就能夠了,無需單獨關注版本問題。

可是,最重要的變化仍是終結了GOPATH做爲Go 代碼工做空間的設置,因爲go.mod文件包含了完整的模塊路徑而且還定義了每一個使用的依賴的版本,所以包含go.mod文件的目錄就能夠被認爲是一個目錄樹的根目錄了,該目錄樹做用於自身的工做空間,而且和其餘相似的目錄彼此隔離。如今你只需git clone而後cd就能夠直接擼代碼了,再也不須要GOPATH

下一步計劃?

我還發布了一篇文章「A Tour of Versioned Go」,主要講了使用vgo的一些感覺。經過這篇文章能夠了解到如今如何下載和體驗vgo,我會在這一週發佈更多的一些文章來補充下這篇文章跳過的一些內容。但願你們能針對這篇文章提出些建議,我也會盡可能去看下 Go subreddit 和 golang-nuts 郵件反饋。在週五我會發布一篇FAQ的終結篇章,下週我將提交一個正式的Go 提案。

請嘗試下vgo吧,開始在你的版本庫中經過tag標記版本吧,建立並檢入go.mod文件。注意若是你的資源庫有一個空的go.mod可是存在dep, glide, glock, godep, godeps, govend,govendor 或者 gvt配置文件的話,vgo將會使用它們填充go.mod文件。

原文: https://research.swtch.com/vg...
相關文章
相關標籤/搜索