GoCenter的“回到未来之旅”

更新:截至2021年5月1日,GoCenter中央存储库已经关闭,所有功能都已弃用。有关中心日落的更多信息,请阅读弃用博客文章
每个科幻迷都知道你无法改变过去。试着去做,它破坏了导致现在的事件的平衡。这也是我们在制作GoCenter时所学到的。而试图纠正一个早期去模块设计选择和提高确定性,我们无意中让Go社区的事情变得更加困难。所以,就像每一个虚构的时间旅行者一样,我们必须追溯我们的脚步来撤销改变。
在我们的GoCenter之旅中,我们学到了一些艰难的教训。通过讲述这个故事,我们可以解释我们最初的推理,分享我们学到的东西,并为将来创建Go模块提供更好的实践建议。
背景
去模块在Go 1.11中被引入,将包版本控制和依赖管理添加到Go生态系统中。使用Go模块,Go开发者可以在他们的模块中声明项目需求go.modfiles and specify which versions of which packages are part of their builds.
当开发人员开始在他们的项目中采用Go模块时,他们面临着一些没有选择提供的依赖项go.mod描述其需求的文件。当Go遇到处于这种状态的模块时,它会自动创建一个go.mod文件,对该模块没有要求。为了让Go模块实现可复制的构建,这种需求信息的缺乏必须得到解决。Go Modules通过添加瞬态依赖项未在go.mod树中依赖于用户模块的文件go.mod与/ /间接发表评论。虽然这可以帮助模块实现可复制的构建,但它无法避免不同模块之间不一致的行为。换句话说,我们可以让不同的模块依赖于相同的直接依赖关系和不同的间接依赖关系,仅仅因为它们是在不同的时间点创建的。
今年年初,JFrog展示了GoCenter,公共的中央Go模块存储库。在GoCenter中,Go模块是不可变的并且总是可用的。Go社区可以利用GoCenter来解决他们的模块需求,并将他们的Go项目CI/CD管道提升到一个新的水平。
当我们开始将项目转换为Go模块以由GoCenter提供服务时,我们面临着同样的缺失。开发人员面临的Mod文件问题。虽然我们了解如何使用提供的Go模块工具来实现可复制的构建,但我们对模块之间可能存在的不一致行为不满意,我们决定对此做点什么,并寻找一个解决方案,让GoCenter提供的所有Go模块都包含Go。描述其需求的Mod文件。
对历史不满意
以确保go.mod要求匹配的模块源代码,Go模块提供Go mod tidy整洁命令。的需求列表中添加源代码所使用的任何缺失的依赖项go.mod当我们试图将项目转换为Go模块时,它可以用于获取需求列表。
在该命令的支持下,我们的第一种方法是实现所有模块的需求go.mod文件的目的是整理所有不包含go.mod文件。
这种方法的第一个问题是,当没有可用的依赖版本时(在我们的下一种方法中有更多关于这一点的内容),Go mod tidy整洁将指向需要语句转换为可用的依赖项的最新版本。这可能会造成损坏的Go模块,因为我们不知道它们是否相互兼容。当我们考虑到指向依赖项的最新版本可以在模块之间创建不可能及时发生的关系时,就更容易理解了。换句话说,通过整理模块,我们可以让一个模块依赖于将来创建的另一个模块。下图说明:

当我们考虑循环引用时,这个问题变得更糟。通过整理作为循环成员的模块,我们可以使模块根本无法解析,因为它将依赖于自身的新版本,并导致Go客户端恐慌。在下面的示例中,模块A@v1.0.0永远不会根据生成的整洁关系进行解析,因为它需要A@v1.1.0作为临时依赖项。
在这一刻,我们意识到过去是很难弥补的。在整理模块时考虑时间变量会增加不必要的复杂性,我们认为这种方法是不可行的。
试图修复过去
在Go模块建立之前,Go社区使用其他依赖管理工具来描述他们的项目需求。这些工具的例子有部,滑翔,govendor。这些工具提供了描述符文件,作者在其中描述了他们的意图,即哪些其他项目和版本需要作为构建的一部分。
为了方便和加快Go模块的采用,Go提供了mod init命令。该命令可以解析其他依赖项管理工具使用的依赖描述符文件,并创建一个go.mod用require语句描述在那里声明的依赖项。
当我们意识到我们不能完全修复过去时,我们决定在将项目转换为Go模块并由GoCenter提供服务时,我们至少应该实现作者在其他依赖管理工具中声明的意图。如果没有提供go.mod文件中,我们将运行mod init使用作者在该命令支持的另一个依赖描述符文件中描述的依赖来创建和填充它。这将修复过去的一部分,我们有足够的可用信息来避免与之前的方法相关的问题。
这种方法引起的第一个问题与验证过程用于Go模块。Go模块介绍了Go。的加密校验和文件go.mod文件和包含模块包的zip归档文件。这些校验和用于验证需求的未来下载,并检测内容中的意外更改。通过运行mod init(甚至Go mod tidy整洁(但我们当时并没有意识到)在项目上并生成GoCenter版本的go.mod文件中,我们将引入根据用于解析依赖项的源而发生的校验和不匹配。
不能期望校验和不匹配,即使它背后有良好的意图。像这样的“预期”校验和不匹配场景会导致用户完全忽略验证过程,当真正的威胁存在时,这可能会导致大问题。
除此之外,有两个孩子go.mod当从VCS解析模块时,一个有GoCenter提供需求的文件版本和一个没有Go提供需求的文件版本可以完全改变一个模块解析的依赖树。

这两个问题与Go模块镜像之间期望的互操作性这使得用户很难在不同的公共Go模块存储库之间切换。因此,我们决定恢复这个功能。
把事情做好
一旦我们明白改变过去的任何东西可能会导致严重的不良后果,我们决定专注于提供与用户所知道的行为同步的体验。
要实现这一点,对于没有go.mod文件,GoCenter将提供一个没有要求的文件。这与Go客户端从VCS解析模块时提供的行为相同。对于已经有go.mod文件,GoCenter将按原样提供它们。这种方法消除了我们试图修复过去所带来的所有痛苦,同时提高了GoCenter与其他公共服务之间的互操作性Go模块注册表。
在处理和验证模块时,GoCenter仍然使用go mod命令,如Go mod tidy整洁和Go模图要发现所有的需求,需要可用的模块是完全可解析的,但我们不使用这些命令的结果来更改go.modGoCenter提供的文件。
因为我们必须在GoCenter上线后进行此更改,所以我们需要清理几个go.mod并将其替换为GoCenter自带的“无需求”版本。你可以得到更多的信息在这里。
面对未来
我们从弥补过去中学到了一些教训:
- 因为我们不能回到过去,所以很难重现事情发生的背景;
- 正因为如此,很难避免遇到产生不可预测结果的矛盾(比如将来创建的依赖模块);
- 这使得预测变化的所有后果变得非常困难。
为了避免在将来再次经历所有这些,我们建议开源go项目的作者使用这些实践,以改进社区发现和使用go依赖项的方式。
采用Go模块
从Go 1.13开始,Go模块将成为默认的依赖管理工具,之前的GOPATH方法将被弃用。项目作者应该从其他工具转向Go模块。如果你的一些依赖还没有转换为Go模块,请让作者采用它。提供一个尝试。具有正确需求列表的Mod文件是我们在使用模块的任何上下文中或场景中实现可复制构建的唯一方法。
Go作者和GoCenter等社区的其他成员都计划了一些关于Go模块的其他特性。除非你开始学习Go模块,否则你将无法从中受益。
避免使用伪版本
提交哈希是VCS的概念,而不是依赖管理的概念。它们带来了混乱,并使人们更难跟随版本的进展。提交哈希伪版本是为了给未标记的项目带来Go模块支持而引入的,它们应该只作为后备机制使用。如果您需要的依赖项有release标签,请在require语句中使用这些标签。如果他们不这样做,要求作者开始标记他们的版本。这就引出了下一个问题。
标签发布遵循语义版本控制规则
虽然你仍然可以在Go模块中使用提交哈希的伪版本,但你应该总是创建语义上有效的标记为你的释放。除了语义版本控制提供的模块兼容性好处之外,标记发布版本可以让用户和GoCenter等服务更容易检测到模块可用的新版本。这可以减少社区了解您的更改和修复所需的时间。
避免使用Replace语句
在您采用Go模块的过程中,您可能会尝试使用replace语句来避免必须修复遍布源代码的import语句,特别是在您即将摆脱依赖关系的情况下。虽然这是一种有效且受支持的技术,替换语句是一个仅用于主模块的指令,对于将你的模块作为依赖项来消费的用户没有任何好处,这将导致你的模块崩溃。
这可能是一个枯燥而痛苦的过程,但修复这些导入并删除replace语句是使模块作为依赖项被所有用户使用而不需要任何额外步骤的唯一方法。
结束
从我们的GoCenter之旅中获得的这些简单的见解,我们希望我们可以照亮过去的步骤,并使整个Go社区的Go模块前进的道路不那么坎坷。