(转)Go项目的vendor目录是否需要提交?看这一篇就知道了
如果您还在使用vendor机制管理依赖包,那么说明您肯定是处于下面两种情况之一!
-
还工作在传统的GOPATH模式下(使用Go 1.10及之前版本;或Go 1.11及之后版本,但GO111MODULE=off),利用vendor管理目标包的特定依赖;
-
工作在go module模式下,但仍然利用vendor管理目标module的特定依赖并使用go build -mod=vendor来构建。
那么我们是否应该将项目中存储依赖包的vendor目录提交到源代码仓库进行管理呢?如果让笔者给出答案,那就是:应该。
要想理解为什么“应该”,我们看看下面Go语言包依赖管理的演化过程就知道了。
Go语言在构建设计方面深受Google内部开发实践模型的影响。
Google内部基于主干的开发模型:
- 所有开发人员基于主干trunk/mainline开发:提交到trunk或从trunk获取最新的代码(同步到本地workspace)
- 版本发布时,建立Release branch,release branch实质上就是某一个时刻主干代码的快照;
- 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit),然后再cherry-pick到release branch上
Go最初的构建管理以及go get就采用了基于[Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发构建模型]。具体逻辑是:在[Go 1.5版本]之前,go get获取的都是各个Go包所在仓库的trunk/mainline的最新代码。go get会将获取的最新代码放在$GOPATH/src下面,而go build会在$GOROOT/src和$GOPATH/src下面按照包导入路径(import path)去搜索这些包并执行构建操作。
我们看到1.5版本之前Go编译器都是基于目标Go程序依赖包的trunk/mainline上的最新代码去编译的,这样的机制带来的问题是显而易见的,至少包括几点:
-
因依赖包的trunk的变化,导致不同人获取和编译你的包/程序时得到的结果实质是不同的,即构建结果不能重现;
-
因依赖包的trunk的变化,引入不兼容的实现,导致你的包/程序无法通过编译;
-
因依赖包演进而无法通过编译,导致你的包/程序无法通过编译。
为了实现可重现的构建(reproduceable build),Go语言于1.5版本引入了[vendor机制]:即Go编译器会优先在vendor目录下搜索依赖的第三方包,这样如果开发者将特定版本的依赖包存放在vendor下面并提交到代码仓库,那么所有人理论上都会得到同样的编译结果,从而实现可重现的构建。
在Go 1.5发布后的若干年,Gopher们把注意力都集中在如何利用vendor解决包依赖问题,从手工添加依赖到vendor、手工更新依赖,到一众包依赖管理工具的诞生:比如: [govendor]、[glide]以及当时号称准官方工具的[dep],都在努力地尝试着按照当今主流思路解决着诸如:“钻石型依赖”等难题。
但Go核心开发团队没有走寻常路,而是另辟蹊径地在[Go 1.11]中引入了采用了[最小版本选择(mvs)]的go module。至此,Go的构建模式被一分为二:gopath mode和module-aware mode。在module-aware mode下,Go构建工具链默认不再使用传统GOPATH下或顶层vendor下面的包了,而是使用$GOPATH/pkg/mod下面的第三方依赖Go module的local cache。理论上,go module真正实现了“可重复的构建”,我们无需再使用Go 1.5引入的vendor机制了。但社区的反馈让Go核心开发团队[将module顶层目录下的vendor目录保留了下来],主要考虑vendor还能在下面场合“发光发热”:
- 保持Go1兼容性
可继续支持[Go 1.5]以后,[Go 1.10]之前的Go版本编译Go 1.11后续版本的源码(仅限于:启用了module并带有vendor)。
- 支持离线构建(offline build)
module/包构建所需的全部依赖都放入了vendor目录,这样即便在无网络连接的情况下,我们依然可以进行module的构建。这尤其适合企业内部执行CI/CD的那些可能没有外网访问权限的主机。
- 提高构建性能,缩短CI/CD时间
在CI/CD时,由于每次都是重新构建,在module-aware模式(非vendor)下,每次都需要重新下载依赖的module到本地,这样十分耗时。而采用vendor方式则无需下载依赖module,提高了构建性能,缩短CI/CD的时间。
- 解决“消失的包/module”的问题
一些module/包在经年岁月后可能被从github等托管站点删除了,这时我们如果依赖这些module/包,我们将遇到构建错误(Go Proxy的存在显然让这种可能行极大的降低了)。而使用vendor已经将包/module存放到了本地(以及自己的代码仓库中),可以解决“包/module消失”的问题。
- 快速分发module的所有依赖包
vendor目录下存放了当面module的所有依赖包(及版本),易于打包并分发。尤其对一些无法通过go get获取到的依赖包/module,这尤为适用。
上述“演化简史”反复提到了**“可重复构建”**,这就是Go核心团队先后推出vendor、go module所基于的核心“痛点”。并且“可重复构建”不单单是个人行为,更多是一个“团队(可以扩展到整个Go社区)”行为:让团队所有人拿到同样的代码并构建出同样的成果物。这样来看,如果不将vendor提交到源码仓库,我们就无法实现这一目标。
在将vendor提交到代码仓库过程中,你也许会抱怨依赖的代码包太多、依赖变化频繁的问题。但go module所使用的[“最小版本选择”]已经将依赖变动降低到不能再低的程度了,至少比采用主流“依赖管理”思路的其他语言,比如js,构建时面临的变动要少很多了。另外降低依赖的代码包的数量也是你自己的责任,Go是“自带电池”的编程语言,其标准库中有很多优秀的包可用,尽量使用标准库包以降低过多的“依赖”。