Go 博客

Go 包版本管理提案

Russ Cox
2018 年 3 月 26 日

引言

八年前,Go 团队推出了 goinstall(后来演变为 go get),以及与之配套的、Go 开发者如今所熟知的去中心化的、类 URL 的导入路径。在发布 goinstall 之后,人们最常问的问题之一是如何包含版本信息。我们承认当时我们并不知道答案。长期以来,我们认为包版本管理问题最好通过一个附加工具来解决,并鼓励大家创建这样一个工具。Go 社区因此产生了许多采用不同方法的工具。每一个工具都帮助我们更好地理解了这个问题,但到了 2016 年年中,很明显解决方案已经太多了。我们需要采纳一个单一的官方工具。

在 2016 年 7 月 GopherCon 上启动并持续到秋季的社区讨论之后,我们都认为答案应该是遵循 Rust 的 Cargo 所体现的包版本管理方法,即使用带标签的语义版本、一个清单文件、一个锁定文件和一个 SAT 求解器 来决定使用哪个版本。Sam Boyer 领导的团队创建了 Dep,它遵循了这个大致的计划,并打算作为 go 命令集成的模型。但随着我们对 Cargo/Dep 方法的更深入理解,我意识到 Go 会从改变一些细节中受益,尤其是在向后兼容性方面。

兼容性的影响

Go 1 最重要的新特性并非语言本身。而是 Go 1 对向后兼容性的强调。在此之前,我们大约每月发布一次稳定的发布快照,每次都包含重大的不兼容更改。我们观察到,在 Go 1 发布后,大家对 Go 的兴趣和采用度立即显著加速。我们相信,兼容性承诺让开发者在生产环境中使用 Go 时感到更加安心,这也是 Go 如今如此受欢迎的关键原因。自 2013 年以来,Go FAQ 一直鼓励包开发者向用户提供类似的兼容性预期。我们称之为导入兼容性规则:“如果一个旧包和一个新包具有相同的导入路径,那么新包必须向后兼容旧包。”

与此同时,语义版本管理已成为许多语言社区(包括 Go 社区)描述软件版本的事实标准。使用语义版本管理,预期后续版本在单一主版本内向后兼容早期版本:v1.2.3 必须兼容 v1.2.1 和 v1.1.5,但 v2.3.4 不需要兼容其中任何一个。

如果我们将语义版本管理应用于 Go 包,正如大多数 Go 开发者所期望的那样,那么导入兼容性规则要求不同的主版本必须使用不同的导入路径。这一观察使我们提出了语义导入版本管理,其中 v2.0.0 及以上的版本在导入路径中包含主版本号:my/thing/v2/sub/pkg

一年前,我坚信是否在导入路径中包含版本号很大程度上是品味问题,并且我怀疑拥有它们是否特别优雅。但这个决定turns out to be a matter not of taste but of logic: 导入兼容性和语义版本管理共同要求语义导入版本管理。当我意识到这一点时,这种逻辑上的必然性令我感到惊讶。

我也惊讶地发现,存在第二种独立的逻辑途径可以通向语义导入版本管理:渐进式代码修复或部分代码升级。在一个大型程序中,不可能期望该程序中的所有包同时从某个依赖项的 v1 更新到 v2。相反,必须允许程序的一部分继续使用 v1,而其他部分已升级到 v2。但那时,程序的构建以及程序的最终二进制文件必须同时包含该依赖项的 v1 和 v2。给它们相同的导入路径会导致混乱,违反我们可能称之为导入唯一性规则:不同的包必须有不同的导入路径。要实现部分代码升级、导入唯一性语义版本管理,唯一的方法就是也采用语义导入版本管理。

当然,可以构建使用语义版本管理而无需语义导入版本管理的系统,但代价是放弃部分代码升级或导入唯一性。Cargo 通过放弃导入唯一性来允许部分代码升级:给定导入路径在大型构建的不同部分可能具有不同的含义。Dep 通过放弃部分代码升级来确保导入唯一性:大型构建中涉及的所有包都必须找到一个单一的、一致同意的依赖项版本,这增加了大型程序可能无法构建的可能性。Cargo 在坚持部分代码升级方面是正确的,这对于大规模软件开发至关重要。Dep 在坚持导入唯一性方面同样是正确的。Go 当前 vendoring 支持的复杂用法可能会违反导入唯一性。当这种情况发生时,由此产生的问题对开发者和工具都带来了极大的挑战。在部分代码升级和导入唯一性之间做出选择,需要预测放弃哪个会更痛苦。语义导入版本管理使我们避免了这个选择,并两者兼顾。

我也惊讶地发现,导入兼容性在多大程度上简化了版本选择,也就是为给定构建决定使用哪个包版本的难题。Cargo 和 Dep 的约束条件使得版本选择等同于求解布尔可满足性,这意味着确定是否存在有效的版本配置可能非常昂贵。然后,可能存在许多有效的配置,但没有明确的标准来选择“最佳”配置。相反,依赖于导入兼容性可以让 Go 使用一个简单的、线性时间算法来找到总是存在的、唯一的最佳配置。这个算法,我称之为最小版本选择,反过来消除了对单独的锁定和清单文件的需求。它用一个单一的、简短的配置文件替换了它们,该文件由开发者和工具直接编辑,同时仍然支持可重复构建。

我们使用 Dep 的经验证明了兼容性的影响。遵循 Cargo 和早期系统的领导,我们在采用语义版本管理时,设计 Dep 时放弃了导入兼容性。我不认为我们是故意这样做的;我们只是遵循了其他系统。使用 Dep 的第一手经验帮助我们更清楚地了解了允许不兼容的导入路径会带来多少复杂性。通过引入语义导入版本管理来恢复导入兼容性规则,消除了这种复杂性,从而得到一个更简单的系统。

进展、一个原型和一个提案

Dep 于 2017 年 1 月发布。它的基本模型——用语义版本标记的代码,以及一个指定依赖项需求的配置文件——相比大多数 Go vendoring 工具是一个明显的进步,而最终统一到 Dep 本身也是一个明显的进步。我全心全意地鼓励大家采用它,尤其是帮助开发者习惯于考虑 Go 包版本,无论是他们自己的代码还是他们的依赖项。虽然 Dep 显然在将我们推向正确的方向,但我对细节中的复杂性问题一直有些担忧。我特别担心 Dep 缺乏对大型程序渐进式代码升级的支持。在 2017 年的整个过程中,我与许多人进行了交谈,包括 Sam Boyer 和其他包管理工作组成员,但我们都没有看到任何明显的方法可以降低复杂性。(我确实找到了许多增加复杂性的方法。)到了年底,SAT 求解器和无法满足的构建似乎仍然是我们所能做到的最好。

11 月中旬,我再次尝试解决 Dep 如何支持渐进式代码升级的问题,我意识到我们关于导入兼容性的旧建议暗含了语义导入版本管理。这似乎是一个真正的突破。我写了第一稿,后来成为了我的语义导入版本管理博文,并在结尾建议 Dep 采用该约定。我将草稿发送给了我一直在交谈的人,并得到了非常强烈的回应:每个人都喜欢它或讨厌它。我意识到,在进一步传播这个想法之前,我需要解决更多语义导入版本管理的含义,于是我开始着手去做。

12 月中旬,我发现导入兼容性和语义导入版本管理结合起来可以使版本选择简化为最小版本选择。我写了一个基本的实现来确保我理解它,花了一些时间学习它为什么如此简单的理论,并写了一篇描述它的博文草稿。即便如此,我仍然不确定这种方法在 Dep 这样的实际工具中是否可行。显然需要一个原型。

1 月份,我开始着手开发一个简单的 go 命令包装器,它实现了语义导入版本管理和最小版本选择。简单的测试运行良好。到了月末,我的简单包装器就可以构建 Dep 了,这是一个使用了许多版本化包的实际程序。包装器仍然没有命令行界面——它构建 Dep 的事实硬编码在几个字符串常量中——但这种方法显然是可行的。

我花了 2 月份的前三周将包装器变成一个完整的版本化 go 命令,名为 vgo;撰写了一系列介绍 vgo 的博文的草稿;并与 Sam Boyer、包管理工作组和 Go 团队进行了讨论。然后,在 2 月份的最后一周,我终于将 vgo 及其背后的想法与整个 Go 社区分享了。

除了导入兼容性、语义导入版本管理和最小版本选择的核心思想外,vgo 原型还引入了许多较小但重要的更改,这些更改源于八年来使用 goinstallgo get 的经验:一个名为Go 模块的新概念,它是一个作为一个单元进行版本管理的包集合;可验证和已验证的构建;以及贯穿 go 命令的版本感知,从而实现了 $GOPATH 之外的工作,以及(大多数)vendor 目录的消除。

所有这些工作的成果就是我上周提交的官方 Go 提案。尽管它看起来可能是一个完整的实现,但它仍然只是一个原型,我们需要共同努力来完成它。您可以从golang.org/x/vgo下载并试用 vgo 原型,您可以阅读版本化 Go 之旅来了解使用 vgo 是什么样的。

前进的道路

我上周提交的提案确实只是一个初步的提案。我知道 Go 团队和我自己看不到其中的问题,因为 Go 开发者以我们不知道的许多巧妙的方式使用 Go。提案反馈过程的目标是让我们共同努力,识别并解决当前提案中的问题,以确保将在未来 Go 版本中发布的最终实现能够很好地服务于尽可能多的开发者。请在提案讨论 issue 中指出问题。随着反馈的到来,我将保持讨论摘要常见问题解答的更新。

为了使该提案成功,整个 Go 生态系统——尤其是当今主要的 Go 项目——将需要采纳导入兼容性规则和语义导入版本管理。为了确保这能够顺利进行,我们还将通过视频会议与有问题的项目进行用户反馈会议,讨论如何将新的版本管理提案纳入其代码库,或者提供关于他们经验的反馈。如果您有兴趣参加这样的会议,请发送电子邮件至 Steve Francia,邮箱为 spf@golang.org。

我们期待(终于!)为 Go 社区提供一个单一的官方答案,来解决如何将包版本管理融入 go get 的问题。感谢所有帮助我们走到这一步的人,以及所有将继续帮助我们的人。我们希望,在您的帮助下,我们能够交付 Go 开发者喜爱的东西。

下一篇文章:Go 的新品牌
上一篇文章:Go 2017 年调查结果
博客索引