文章写于2018年2月。Go需要添加软件包版本控制。
更准确地说,您需要在Go开发人员的工作词典和工具中添加版本控制的概念,以便每个人在提及要构建,运行或分析的程序时都使用相同的版本号。
go
命令应准确说明特定程序集中哪个软件包的哪个版本。
版本编号使您可以制作可重现的程序集:如果我发布程序的最新版本,则不仅会获得代码的最新版本,而且还会获得代码所依赖的所有软件包的完全相同的版本,因此我们将创建完全等效的二进制文件。
版本控制还可以确保明天的程序构建与今天完全相同。 即使发布了新版本的依赖关系,
go
也不会在没有特殊命令的情况下使用它们。
尽管您需要添加版本控制,但您不应放弃
go
命令的主要优点:简单,快速和易于理解。 如今,许多程序员不再关注版本,并且一切正常。 如果您选择正确的模型,那么程序员
仍然不会关注版本号,只是一切都会更好并且变得更加清晰。 现有的工作流程几乎不会改变。 新版本的发布非常简单。 通常,版本控制应该顺其自然,不要引起开发人员的注意。
简而言之,您需要添加软件包的版本控制,但不要破坏
go get
。 在本文中,我们建议如何执行此操作,并演示您现在可以尝试使用的原型,并且希望它将成为可能进行
go
集成的基础。 我希望本文将成为关于什么有效和什么无效的富有成效的讨论的开始。 在此讨论的基础上,我将对我的建议和原型进行调整,然后将
提出为Go 1.11添加可选功能的
正式建议 。
该建议保留了
go get
所有优点,但增加了可重现的版本,支持语义版本控制,消除了供应商关系,删除了GOPATH以支持基于项目的工作流,并顺利脱离了
dep
及其前身。 但是,该建议仍处于早期阶段。 如果细节不正确,我们将在工作进入主Go发行版之前对其进行修复。
一般情况
在研究提案之前,让我们看一下当前情况以及我们最终是如何完成的。 本部分可能太大,但是历史带来了重要的教训,并有助于理解为什么我们要更改某些内容。 如果您对此不太感兴趣,则可以立即转到
报价或
通过示例阅读
随附的博客文章 。
Makefile
, goinstall
和go get
2009年11月,Go的初始版本发布了编译器,链接器和一些库。 要编译和链接程序,必须运行
6g
和
6l
,并且我们在工具包中包含了示例makefile。 最小的
gobuild
shell可以编译一个软件包并编写相应的makefile(在大多数情况下)。 没有建立与他人共享代码的方法。 我们知道这还不够-但是我们释放了已有的东西,并计划与社区一起开发其余的东西。
在2010年2月,
我们提出了goinstall ,这是一个简单的命令,可以从版本控制系统(例如Bitbucket和GitHub)的存储库中下载软件包。
Goinstall
在导入路径上引入了约定,这些约定现在已被普遍接受。 但是在那时,没有代码遵循这些约定;
goinstall
最初仅适用于除了标准库之外不导入任何内容的软件包。 但是开发人员很快就达成了我们今天所知的一项协议,并且已发布的Go软件包集已发展成为一个整体的生态系统。
Goinstall还修复了Makefile,以及它们的自定义构建选项的复杂性。 尽管有时包创建者无法在每次构建期间生成代码很不方便,但对于包
用户而言 ,这种简化极为重要:他们不必担心安装与作者使用的同一套工具。 这种简化对于工具的操作也至关重要。 Makefile是编译软件包所必需的逐步食谱; 并在同一个程序包中应用其他工具,例如
go vet
或autocompletion可能非常困难。 即使是正确获取依赖关系,如果有必要并且仅在必要时才重建软件包,使用任意Makefile会更加复杂。 尽管当时有人反对他们失去灵活性,但回头一看,很明显,放弃Makefile是正确的步骤:带来的好处远大于带来的不便。
在2011年12月,为准备Go 1,
我们引入了go命令 ,该
命令将
goinstall
替换为
go get
。
总的来说,
go get
了重大的变化:它允许Go开发人员交换源代码并使用彼此的工作。 他还在
go
命令中隔离了构建系统的各个部分,从而使借助工具的大量自动化成为可能。 但是缺乏版本控制的概念。 在
goinstall的最初讨论中,很明显:您需要使用版本控制来做一些事情。 不幸的是,目前尚不清楚该怎么做。 至少我们Go团队中的成员并不清楚。 当
go get
请求一个软件包时,它总是获取最新的副本,将下载和更新操作委派给版本控制系统(例如Git或Mercurial)。 这种“盲目的工作”至少导致了两个重大缺陷。
版本控制和API稳定性
go get
的第一个主要缺点是,如果没有版本控制的概念,它将无法告诉用户有关此更新中预期要进行的更改的任何信息。
2013年11月,Go 1.2版本添加了一个FAQ条目,其中包含有关版本控制的建议(文本未更改为Go 1.10版本):
通用软件包在发展时应保持向后兼容性。 Go 1兼容性建议与此处相关:不要删除导出的名称,鼓励标记复合文字,等等。 如果需要新功能,请添加新名称,而不要更改旧名称。 如果发生根本变化,请使用新的导入路径创建一个新程序包。
2014年3月,古斯塔沃·尼迈耶(Gustavo Niemeyer)以“ Go语言的稳定API”为幌子启动了
gopkg.in 。 该域是
gopkg.in/yaml.v1
GitHub重定向,允许您导入
gopkg.in/yaml.v1
和
gopkg.in/yaml.v2
类的
gopkg.in/yaml.v1
,
gopkg.in/yaml.v2
用于一个Git存储库的各种提交(可能在不同的分支中)。 根据语义版本控制,作者应在进行关键更改时发布新的主要版本。 因此,
v1
导入路径的更高版本将替换先前的版本,而
v2
可以提供完全不同的API。
2015年8月,Dave Cheney
提交了有关语义版本控制的提案 。 在接下来的几个月中,这引起了一个有趣的讨论:每个人似乎都同意版本的语义标记是一个好主意,但是没人知道工具应该如何与这些版本一起使用。
关于语义版本控制的任何论点都将不可避免地受到
Hyrum定律的批评:
您的API合同对于足够多的用户而言不再重要。 某人取决于观察到的系统行为。
尽管Hyrum定律在经验上是正确的,但语义版本控制仍然是产生对发行版之间关系期望的有用方法。 从1.2.3升级到1.2.4不会破坏您的代码,从1.2.3升级到2.0.0可能很好。 如果代码在更新到1.2.4后停止工作,那么作者很可能会接受错误报告并修复1.2.5版中的错误。 如果代码更新到2.0.0后停止工作(甚至编译),则此更改很有可能是有意的,因此,不太可能在2.0.1中修复某些问题。
我不想从希兰姆定律得出结论,语义版本化是不可能的。 相反,我认为应谨慎使用程序集,每个依赖项的版本应与作者完全相同。 也就是说,默认程序集应尽可能地重现。
自动售货和可复制装配
go get
的第二个主要缺点是,如果没有版本控制的概念,团队就无法提供甚至无法表达可复制构建的想法。 您不能确定用户正在编译与您相同版本的代码依赖项。 2013年11月,将以下常见问题解答添加到Go 1.2的常见问题解答中:
如果您使用外部软件包,并且担心它可能会发生意外更改,最简单的解决方案是将其复制到本地存储库(Google使用此方法)。 用新的导入路径保存一个副本,该路径将其标识为本地副本。 例如,您可以将original.com/pkg
复制到you.com/external/original.com/pkg
。 该过程的工具之一是Kit Rerik的goven。
Keith Rarik于2012年3月开始了这个项目。
goven
实用程序将依赖项复制到本地存储库,并更新所有导入路径以反映新位置。 这样的源代码更改是必要的,但令人讨厌。 它们使比较和添加新副本变得困难,并且还需要使用此依赖项来更新其他复制的代码。
2013年9月,
基思(Keith)推出了godep ,“一种用于修复软件包依赖关系的新工具”。
godep
的主要成就是我们现在所说的供应商,即通过以某种方式设置GOPATH,将依赖项复制到项目中
而无需更改源文件,无需直接支持工具。
2014年10月,Keith建议在Go工具中添加
对“外部软件包”的支持,以使工具可以更好地理解使用此约定的项目。 到那时,几种
godep
风格的实用程序已经出现。 马特·法里纳(Matt Farina)发表了一篇文章“旅行到
godep
管理者的海洋”,将
godep
与
godep
进行了比较,尤其是
glide
。
2015年4月,Dave Cheney再次
引入gb (“基于项目的构建工具...通过源代码售卖进行重复构建)”,而无需重写导入路径(创建gb的另一个动机是避免了将代码存储在GOPATH中的特定目录中的要求)这并不总是很方便)。
那个春天,Jason Buberlie使用Go软件包管理系统检查了这种情况,其中包括重复工作和在类似工具上徒劳的工作。 他的调查向开发人员明确表明,必须在
go
命令中添加对无需出售导入路径即可进行销售的支持。 同时,Daniel Theofanes开始准备一种文件格式的规范,以描述供应商目录中代码的确切来源和版本。 2015年6月,我们接受了基思的提议,将其作为
Go 1.5中自动售货的
实验 ,该提议默认包含在Go 1.6中。 我们鼓励所有自动售货工具的作者与Daniel一起采用单一元数据文件格式。
在Go中引入自动售货概念后,像
vet
这样的工具就可以更有效地分析程序,而如今,一打或两个程序包管理器或自动售货工具已使用它。 另一方面,由于每个人都有不同的元数据格式,因此它们不会交互并且无法轻松交换依赖项信息。
从根本上讲,自动售货是版本控制问题的不完整解决方案。 它仅提供程序集的可复制性,但无助于了解程序包的版本并决定使用哪个版本。 像
glide
和
dep
这样的包管理器隐式地将版本控制的概念添加到Go中,从而以某种方式设置了供应商目录。 结果,Go生态系统中的许多工具可能无法获取正确的版本信息。 显然,Go需要直接支持软件包版本。
官方包装管理实验
在Hack Day(现在的社区日)的GopherCon 2016上,一群Go活动家聚集在一起,
广泛讨论了软件包管理问题 。 结果之一就是成立了一个
委员会和一个咨询小组,以开展一系列活动,以期创建一种新的软件包管理工具 。 这个想法是要有一个统一的工具来替换现有的工具,尽管它仍然可以使用供应商目录在Go的直接工具包之外实现。 该委员会由彼得·布尔贡(Peter Burgon)领导的安德鲁·格朗(Andrew Gerrand),埃德·穆勒(Ed Muller),杰西·弗雷泽(Jesse Frazel)和萨姆·博耶(Sam Boyer)。 他们准备了
一份规范草案 ,然后Sam和他的助手
实施了dep 。 要了解一般情况,请参阅Sam在2016年2月发表的文章
“您想写
一个程序包管理器”,他在2016年12月发布的
“ Go Dependency Management Saga”和他在2017年7月在GopherCon上的演讲
“程序包管理的新时代”。走吧 。
“Dep
执行许多任务:这是对当前实践的重要改进。 这是迈向未来解决方案的重要一步,同时也是一项实验-我们称其为“官方实验”-可帮助我们更好地了解开发人员的需求。 但是
dep
并不是将
go
命令集成在软件包版本控制中的直接原型。 这是一种探索设计决策空间的强大,灵活,几乎通用的方法。 它类似于我们一开始就争夺的makefile。 但是,一旦我们更好地了解设计决策的空间并将其范围缩小到应支持的几个关键功能,这将有助于Go生态系统删除其他功能,降低表达性并采用强制性约定,从而使Go代码库更加一致且更易于理解。
本文是
dep
之后下一步的开始:最终与
go
命令集成的第一个原型,相当于
goinstall
的批处理。 原型是一个单独的命令,我们称为
vgo
:支持软件包版本控制的
go
替换。 这是一个新实验,我们将看到它的结果。 在
goinstall
声明期间,某些项目和代码现在与
vgo
兼容,而其他项目和代码则需要更改。 我们将删除一些控制和表达,就像删除makefile一样,以简化系统并消除用户的复杂性。 最重要的是,我们正在寻找能够帮助
vgo
获得尽可能多评论的开拓者。
使用
vgo
进行实验并不意味着停止对
dep
支持:它将一直可用,直到我们使用
go
实现完全开放的集成。 我们还将尝试使从
dep
到集成的最终过渡尽可能地顺利进行,无论这种集成发生什么形式。 尚未转换为
dep
仍可以从此转换中受益(请注意,
godep
和
glide
停止积极开发,并鼓励迁移到dep)。 如果满足他们的需求,也许某些项目可能希望直接切换到
vgo
。
提供
向
go
命令添加版本控制的建议包括四个步骤。 首先,接受FAQ和gopkg.in指示的
导入兼容性规则 :具有指定导入路径的软件包的新版本必须与旧版本向后兼容。 其次,采用一种简单的新算法,即
选择最低版本以确定该程序集中使用哪个版本的程序包。 第三,介绍Go
模块的概念:整个版本化的软件包组,并声明其依赖项必须满足的最低要求。 第四,确定如何将所有这些集成到您现有的
go
命令中,以使基本工作流从今天开始就不会有太大变化。 在本文的其余部分中,我们将研究每个步骤。
其他博客文章中对它们进行了更详细的讨论。
导入兼容性规则
软件包管理系统的主要问题是试图解决不兼容问题。 例如,大多数系统允许程序包B声明它需要版本6或更高版本的程序包D,然后允许程序包C声明它需要D版本2、3或4,而不是版本5或更高版本。 因此,如果您想在软件包中使用B和C,那么您就不走运了:您不能选择同时满足这两个条件的任何版本的D,并且您什么也不能做。
我们的建议不是为系统不可避免地阻止大型程序的汇编,而是为程序包作者引入了
导入兼容性规则 :
如果新旧软件包的导入路径相同,则新软件包必须与旧软件包向后兼容。
该规则重复前面提到的FAQ。 该文本的结尾为:“如果发生重大变化,请使用新的导入路径创建一个新软件包。”
今天,对于如此重大的更改,开发人员指望语义版本控制,因此我们将其集成到我们的建议中。特别是,第二个及后续主要版本的数量可以直接包含在路径中: import "github.com/go-yaml/yaml/v2"
在语义版本控制中,版本2.0.0意味着根本性的更改,因此将使用新的导入路径创建一个新包。由于每个主要版本都有不同的导入路径,因此特定的Go可执行文件可能包含其中一个主要版本。这是预期的和可取的。这样的系统支持程序的汇编,并允许非常大的程序的各个部分独立地从v1升级到v2。作者遵守导入兼容性规则,从而通过指数简化了整个系统并减少了软件包生态系统的碎片,从而消除了解决不兼容问题的尝试。当然,在实践中,尽管作者做出了所有努力,但相同主版本中的更新有时会破坏用户软件包。因此,您不应该经常更新。这将我们带入下一步。最低版本选择
,
dep
cargo
,
. , . -, « » - , - . , - , , , . -, , , «, X»,
X .
我们的建议使用了另一种方法,我将其称为“最低版本”。默认情况下,使用每个软件包的最早允许版本。该决定明天不会更改,因为不可能发布较旧的版本。更好的是,打包管理器确定要使用哪个版本很简单。我称此为最低版本的选择,因为选择的版本号最小,并且因为整个系统可能也最小,从而避免了几乎所有现有系统的复杂性。选择最低版本允许模块仅指定依赖性的最低要求。这些是针对更新和降级操作的明确定义的唯一答案,并且这些操作确实有效。该原理允许整个模块的作者指示他要排除的依赖项的版本,或者指示将特定的依赖项替换为其派生的分支(位于本地存储中或作为单独的模块发布)。当模块作为其他模块的依赖项构建时,这些例外和替换不适用于这些模块。这使用户可以完全控制自己程序的组装方式,而不能控制陌生人。选择最低版本默认情况下会提供可复制的程序集,而没有锁定文件。导入兼容性是使最低版本易于选择的关键。用户不能再说“不,这是太新的版本”,而只能说“不,这是太旧的版本”。在这种情况下,解决方案很明确:使用(最低)更新版本。按照惯例,较新的版本可以替代较旧的版本。定义Go模块
Go 模块是具有公共导入路径前缀(称为模块路径)的软件包的集合。模块是版本控制单元,版本被写为语义字符串。使用Git进行开发时,开发人员可以通过向模块的Git存储库中添加标签来定义模块的新语义版本。尽管强烈建议您指定语义版本,但也支持指向特定提交的链接。在新文件中,go.mod
模块定义了其依赖的其他模块的最低版本要求。例如,这是一个简单的文件go.mod
:
该文件定义了一个由path标识的模块,rsc.io/hello
它依赖于其他两个模块:golang.org/x/text
和rsc.io/quote
。模块程序集本身将始终使用文件中列出的必需依赖项的某些版本go.mod
。作为较大装配的一部分,如果装配的其他部分需要,则只能使用较新的版本。作者使用语义版本标记其发布,并vgo
建议使用标记版本,而不是任意提交。rsc.io/quote
随附的模块github.com/rsc/quote
具有标记的版本,包括1.5.2。但是,该模块尚无golang.org/x/text
标记版本。要命名未标记的提交,请使用伪版本v0.0.0-yyyymmddhhmmss-commit定义给定日期的特定提交。在语义版本控制中,此行对应于标识符为yyyymmddhhmmss-commit的v0.0.0预发行版。版本优先级的语义规则识别早于v0.0.0版本的此类预发行版本,并执行字符串比较。伪版本中的日期顺序可确保字符串比较与日期比较匹配。除了这些要求之外,文件go.mod
还可以指示上一节中提到的异常和替换,但是它们再次仅在构建隔离模块时适用,而在作为较大程序的一部分进行构建时不适用。所有这些都在示例中得到了证明。Goinstall
和老go get
调用版本控制工具(例如git
和)来下载代码hg
,这会导致许多问题,包括碎片。例如,没有的bzr
用户无法从Bazaar存储库下载代码。与该系统不同,Go模块始终以zip存档的形式通过HTTP发出。以前,go get
流行的代码托管站点有专门的团队。现在,他们有了vgo
特殊的API程序,可以从这些站点接收存档。以zip存档的形式对模块进行统一表示,可以轻松实现协议和代理服务器来加载模块。公司和个人用户具有启动此类代理服务器的不同原因,包括安全性以及在删除原始文件的情况下使用缓存副本的愿望。有了适当的代理,即可确保可访问性并go.mod
确定要使用的代码,从而不再需要供应商目录。团队 go
go
. , ,
go build
,
go install
,
go run
go test
, .
golang.org/x/text
Go .
— GOPATH .
go.mod
, ,
go.mod
, , .
git clone
,
cd
, . . GOPATH.
?
我还发布了Go Versioning Tour,其中包含有关如何使用它的演示vgo
。那篇文章告诉您今天如何下载和开始使用vgo
。其他文章中的其他信息。我很乐意发表评论。请尝试vgo。开始使用语义标记在存储库中标记版本。创建文件go.mod
。请注意,如果在仓库中的空文件go.mod
,但也有dep
,glide
,glock
,godep
,godeps
,govend
,govendor
或者配置文件gvt
,然后vgo
使用它们来填充文件go.mod
。我很高兴Go在版本支持方面迈出了很长的一步。 Go开发人员面临的一些最常见问题是:缺少可复制的版本,从外部完全忽略发行标签go get
,GOPATH无法识别软件包的不同版本以及无法在GOPATH外部的目录中工作。这里提供的设计消除了所有这些问题,甚至更多。但是我可能在某些细节上弄错了。我希望读者可以通过测试原型vgo
并参加富有成效的讨论来帮助改进它。我希望Go 1.11可以提供对Go模块的初步支持,作为一种演示,然后Go 1.12可以得到官方支持。在更高版本中,我们将删除对旧的非版本的支持go get
。但这是一个激进的计划,如果要获得正确的功能,则需要等待更高版本。我非常担心从go get
无数旧的自动售货工具向新的模块化系统的过渡。对我来说,此过程与正确的功能一样重要。如果成功的过渡意味着等待更高版本,那么也可以。