Go程序构建
-
Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程。
-
Go 语言的构建模式历经了三个迭代和演化过程:
- GOPATH
- Go 1.5 版本的 Vendor 机制
- 现在的 Go Module
GOPATH
-
Go 编译器可以在本地 GOPATH 环境变量配置的路径下,搜寻 Go 程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误。
-
go get命令不仅能将包下载到 GOPATH 环境变量配置的目录下,它还会检查包的依赖包在本地是否存在,如果不存在,go get也会一并将它们下载到本地。 -
GOPATH 的缺陷:
- 在 GOPATH 构建模式下,Go 编译器实质上并没有关注 Go 项目所依赖的第三方包的版本。
go get下载的包只是那个时刻各个依赖包的最新主线版本,这样会给后续 Go 程序的构建带来一些问题。 - 依赖包持续演进,可能会导致不同开发者在不同时间获取和编译同一个 Go 包时,得到不同的结果,也就是不能保证可重现的构建(Reproduceable Build)。
- 如果依赖包引入了不兼容代码,程序将无法通过编译。
- 在 GOPATH 构建模式下,Go 编译器实质上并没有关注 Go 项目所依赖的第三方包的版本。
vendor机制
-
vendor 机制本质上就是在 Go 项目的某个特定目录下,将项目的所有依赖包缓存起来,这个特定目录名就是 vendor。
-
实现可重现的构建,如果使用 vendor 机制管理第三方依赖包,最佳实践就是将 vendor 一并提交到代码仓库中。
-
要想开启 vendor 机制,Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。
-
vendor机制的缺陷:
- Go 项目必须放在 GOPATH 环境变量配置的路径下,庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度,而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。
- 需要手工管理 vendor 下面的 Go 依赖包,包括项目依赖包的分析、版本的记录、依赖包获取和存放,等等,最让开发者头疼的就是这一点。
Go Module
-
一个 Go Module 是一个 Go 包的集合,Go Module 与 go.mod 是一一对应的。
-
Go Module 构建步骤:
-
第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
-
第二步,通过 go mod tidy 命令自动更新当前 module 的依赖信息;
- go mod tidy 命令会扫描 Go 源码,并自动找出项目依赖的外部 Go Module 以及版本,下载这些依赖并更新本地的 go.mod 文件。
- 当将来这里的某个 module 的特定版本被再次下载的时候,go 命令会使用 go.sum 文件中对应的哈希值,和新下载的内容的哈希值进行比对,只有哈希值比对一致才是合法的,这样可以确保项目所依赖的 module 内容,不会被恶意或意外篡改。
-
第三步,执行 go build,执行新 module 的构建。
- go build 命令会读取 go.mod 中的依赖及版本信息,并在本地 module 缓存路径下找到对应版本的依赖 module,执行编译和链接。
-
深入 Go Module 构建模式
-
语义导入版本 (Semantic Import Versioning):通过在包导入路径中引入主版本号的方式,来区别同一个包的不兼容版本:
- 按照语义版本规范,主版本号不同的两个版本是相互不兼容的。
- 如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。
- 将包主版本号引入到包导入路径中,可以像下面这样导入 logrus v2.0.0 版本依赖包。
-
最小版本选择 (Minimal Version Selection) :Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。换句话说最终选择的是当前依赖包中版本最高的那个,而不是最新的那个包,遵循的原则是刚刚够用就好,不必太新。优势如下:
- 对开发者而言,更易于理解和预测,根据依赖图可以很容易确定程序构建最终使用的依赖版本。
- 对go核心团队来说,更容易实现,据说实现最小选择的代码也就几十行。
- 最小版本选择”为 Go 程序实现持久的和可重现的构建提供了最佳的方案。更容易实现可重现构建。试想一下,如果选择的是最大最新版本,那么针对同一份代码,其依赖包的最新最大版本在不同时刻可能是不同的,那么在不同时刻的构建,产生的最终文件就是不同的。
Go 各版本构建模式机制和切换
Go Module 操作
- 通过
go get可以升级或降级某依赖的版本,如果升级或降级前后的版本不兼容,这里千万注意别忘了变化包导入路径中的版本号,这是 Go 语义导入版本机制的要求。 go mod tidy命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。go mod tidy会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。- 如果要基于 vendor 构建
go mod vendor,而不是基于本地缓存的 Go Module 构建,需要在 go build 后面加上-mod=vendor参数。 - 如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非给 go build 传入
-mod=mod的参数。