Go 博客
Go 包版本控制提案
引言
八年前,Go 团队推出了 goinstall
(后来演变为 go get
),并随之引入了 Go 开发者今天熟悉的去中心化、类似 URL 的导入路径。在我们发布 goinstall
后,人们提出的首要问题之一是如何整合版本信息。我们承认当时并不知道答案。很长一段时间以来,我们认为包版本控制问题最好通过附加工具来解决,并鼓励人们创造这样的工具。Go 社区开发了许多采用不同方法的工具。每种工具都帮助我们更好地理解了这个问题,但到 2016 年中期,很明显解决方案已经太多了。我们需要采用一个单一的官方工具。
在 2016 年 7 月的 GopherCon 大会开始并持续到秋季的社区讨论之后,我们都认为答案将是遵循 Rust 的 Cargo 所代表的包版本控制方法,即带有标签的语义版本、一个清单文件(manifest)、一个锁定文件(lock file)和一个 SAT 求解器 来决定使用哪些版本。Sam Boyer 带领一个团队创建了 Dep,该工具遵循了这个大致计划,我们原本打算将其作为 go
命令集成的模型。但是,随着我们对 Cargo/Dep 方法含义的了解深入,对我而言,Go 显然可以从修改一些细节中受益,尤其是在向后兼容性方面。
兼容性的影响
Go 1 最重要的新特性不是语言特性,而是 Go 1 对向后兼容性的强调。在此之前,我们大约每月发布一次稳定的版本快照,每个版本都有重大的不兼容变更。我们观察到 Go 1 发布后,关注度和采用率显著加速。我们相信,兼容性承诺 让开发者对在生产环境中使用 Go 感到更加放心,这也是 Go 今天如此流行的关键原因。自 2013 年以来,Go FAQ 一直鼓励包开发者为其用户提供类似的兼容性预期。我们将此称为*导入兼容性规则*(import compatibility rule):“如果旧包和新包具有相同的导入路径,则新包必须与旧包向后兼容。”
此外,语义版本控制 已成为许多语言社区(包括 Go 社区)中描述软件版本的*事实标准*(de facto)。使用语义版本控制,后续版本预计将与早期版本向后兼容,但仅限于同一主版本号内:v1.2.3 必须与 v1.2.1 和 v1.1.5 兼容,但 v2.3.4 不需要与其中任何一个兼容。
如果像大多数 Go 开发者所期望的那样,我们为 Go 包采用语义版本控制,那么导入兼容性规则要求不同的主版本必须使用不同的导入路径。这一观察结果使得我们提出了*语义导入版本控制*(semantic import versioning),其中从 v2.0.0 开始的版本在导入路径中包含主版本号:my/thing/v2/sub/pkg
。
一年前,我坚信是否在导入路径中包含版本号很大程度上是个人偏好问题,并且我怀疑这样做是否特别优雅。但事实证明,这个决定并非关乎偏好,而是关乎逻辑:导入兼容性和语义版本控制共同要求语义导入版本控制。当我意识到这一点时,这种逻辑必然性令我感到惊讶。
令我惊讶的是,我还意识到通往语义导入版本控制的第二条独立的逻辑路径是:渐进式代码修复 或部分代码升级。在一个大型程序中,期望程序中的所有包同时从某个依赖的 v1 更新到 v2 是不现实的。相反,必须允许程序的一部分继续使用 v1,而另一部分已升级到 v2。但这样一来,程序的构建及其最终的可执行文件就必须同时包含该依赖的 v1 和 v2。如果它们使用相同的导入路径,就会导致混淆,违反我们可能称之为*导入唯一性规则*(import uniqueness rule)的原则:不同的包必须有不同的导入路径。实现部分代码升级、导入唯一性*以及*语义版本控制的唯一方法是也采用语义导入版本控制。
当然,构建使用语义版本控制但不使用语义导入版本控制的系统是可能的,但这只能通过放弃部分代码升级或导入唯一性来实现。Cargo 通过放弃导入唯一性来允许部分代码升级:在大型构建的不同部分,同一个导入路径可以有不同的含义。Dep 通过放弃部分代码升级来确保导入唯一性:大型构建中涉及的所有包都必须找到给定依赖的单一、商定的版本,这可能会导致大型程序无法构建。Cargo 坚持部分代码升级是正确的,这对于大规模软件开发至关重要。Dep 坚持导入唯一性同样是正确的。Go 当前 vendoring(供应商依赖)支持的复杂用法可能会违反导入唯一性。当这种情况发生时,由此产生的问题对于开发者和工具来说都非常难以理解。在部分代码升级和导入唯一性之间做出选择,需要预测放弃哪一个会带来更大的痛苦。语义导入版本控制让我们避免了这种选择,并同时保留了两者。
令我惊讶的是,我发现导入兼容性极大地简化了版本选择,版本选择是决定给定构建使用哪些包版本的问题。Cargo 和 Dep 的限制使得版本选择等同于解决布尔可满足性问题,这意味着确定一个有效的版本配置是否存在都可能非常耗时。而且可能存在许多有效的配置,但没有明确的标准来选择“最佳”的一个。相反,依赖导入兼容性可以让 Go 使用一个简单的、线性时间算法来找到唯一最佳的配置,并且这个配置总是存在。我将这个算法称为*最小版本选择*(minimal version selection),它反过来消除了对单独的锁定文件和清单文件的需求。它用一个单一的、简短的配置文件取而代之,这个文件可以直接由开发者和工具编辑,并且仍然支持可重现的构建。
我们使用 Dep 的经验证明了兼容性的影响。遵循 Cargo 和早期系统的思路,我们在设计 Dep 时,作为采用语义版本控制的一部分,放弃了导入兼容性。我不认为这是我们故意决定的;我们只是跟随了那些其他系统。亲身使用 Dep 的经验帮助我们更好地理解了允许不兼容导入路径会产生多少复杂性。通过引入语义导入版本控制来恢复导入兼容性规则,消除了这种复杂性,从而形成了一个更简单的系统。
进展、原型和提案
Dep 于 2017 年 1 月发布。其基本模型——带有语义版本标签的代码,以及指定依赖需求的配置文件——相对于大多数 Go 的 vendoring 工具来说,是一个明显的进步,并且集中使用 Dep 本身也是一个明显的进步。我衷心鼓励大家采用它,特别是帮助开发者习惯于思考 Go 包的版本,无论是对于自己的代码还是依赖。尽管 Dep 显然在朝着正确的方向推进我们,但我仍然对细节中的复杂性感到担忧。我特别担心 Dep 在大型程序中缺乏对渐进式代码升级的支持。在 2017 年全年,我与许多人进行了交流,包括 Sam Boyer 和包管理工作组的其他成员,但我们谁也看不出任何清晰的方法来降低复杂性。(我确实找到了许多增加复杂性的方法。)临近年底时,似乎 SAT 求解器和不可满足的构建可能是我们能做到的最好的了。
在 11 月中旬,我再次尝试思考 Dep 如何支持渐进式代码升级时,我意识到我们关于导入兼容性的旧建议实际上暗示了语义导入版本控制。这似乎是一个真正的突破。我写下了后来成为我的语义导入版本控制博文的初稿,结尾建议 Dep 采用这种约定。我将草稿发送给了我一直在交流的人,它引起了非常强烈的反应:要么非常喜欢,要么非常讨厌。我意识到我需要进一步研究语义导入版本控制的更多影响,然后才能将这个想法传播出去,于是我开始着手这项工作。
在 12 月中旬,我发现导入兼容性和语义导入版本控制结合起来可以将版本选择简化为最小版本选择。我写了一个基本的实现来确保我理解它,花了一些时间学习它为何如此简单的理论基础,并写了一篇描述它的博文草稿。尽管如此,我仍然不确定这种方法是否适用于像 Dep 这样的真实工具。很明显,需要一个原型。
一月份,我开始着手编写一个简单的 go
命令包装器,它实现了语义导入版本控制和最小版本选择。简单的测试运行良好。接近月底时,我的简单包装器可以构建 Dep,这是一个使用了许多版本化包的真实程序。这个包装器仍然没有命令行界面——构建 Dep 这个事实是硬编码在几个字符串常量中的——但这种方法显然是可行的。
我花了二月份的前三周将这个包装器变成一个完整的版本化 go
命令,即 vgo
;撰写介绍 vgo
的系列博文草稿;并与 Sam Boyer、包管理工作组和 Go 团队讨论这些内容。然后我在二月份的最后一周,终于向整个 Go 社区分享了 vgo
及其背后的思想。
除了导入兼容性、语义导入版本控制和最小版本选择这些核心思想之外,vgo
原型还引入了许多由 goinstall
和 go get
八年经验启发而来、规模较小但意义重大的改变:一个Go 模块的新概念,它是一个作为单元进行版本控制的包集合;可验证和已验证的构建;以及go
命令的全面版本感知,这使得可以在 $GOPATH
之外工作并消除了(大多数)vendor
目录。
所有这一切的结果就是官方 Go 提案,我上周提交了它。尽管它看起来可能像一个完整的实现,但它仍然只是一个原型,一个需要我们所有人共同努力才能完成的原型。您可以从 golang.org/x/vgo 下载并试用 vgo
原型,您也可以阅读版本化 Go 之旅来了解使用 vgo
的感觉。
前进之路
我上周提交的提案正是如此:一个初步提案。我知道其中存在一些 Go 团队和我目前看不到的问题,因为 Go 开发者以许多我们不知道的巧妙方式使用 Go。提案反馈过程的目标是我们所有人共同努力识别并解决当前提案中的问题,以确保在未来的 Go 版本中发布的最终实现能够尽可能好地为更多开发者服务。请在提案讨论议题上指出问题。我将根据收到的反馈持续更新讨论摘要和常见问题解答。
为了使这项提案成功,整个 Go 生态系统——特别是当今主要的 Go 项目——需要采用导入兼容性规则和语义导入版本控制。为了确保这一过程顺利进行,我们还将通过视频会议与那些对于如何将新的版本控制提案纳入其代码库有疑问或想分享使用经验反馈的项目进行用户反馈会议。如果您有兴趣参加此类会议,请发送电子邮件至 spf@golang.org 联系 Steve Francia。
我们期待着(终于!)为 Go 社区提供一个关于如何将包版本控制整合到 go get
中的单一官方答案。感谢所有帮助我们走到这一步的人,也感谢所有未来将帮助我们的人。我们希望在您的帮助下,我们能够推出 Go 开发者喜爱的东西。
下一篇文章:Go 的新品牌形象
上一篇文章:Go 2017 年度调查结果
博客索引