要解决的问题
golang vendor 是为了解决 golang 依赖问题的。为什么 Go 的依赖需要专门机制?
原因在于:golang 的依赖是以源码的形式存在的。编译时直接使用依赖包的源代码参与构建。这就带来一个直接风险——如果别的项目变更了 API,这个时候很有可能会让你的代码无法通过编译。
更麻烦的是这类失败往往发生得很突然:你自己的代码一行没改,某天 CI 上构建就挂了,因为上游某个依赖改了函数签名。这就是构建不确定性的典型表现。
vendor 之前的做法:godep
在 vendor 出来之前,一般都是用 godep 来解决的。它的做法是:
- 把这些依赖包的源代码复制到当前项目的 Godeps 目录下;
- 把构建命令从
go build变成godep go build。
这样做确实锁定了依赖版本,但问题也很明显:
- 改变了 go build 的原义:团队必须统一使用包装后的命令,一旦有人习惯性敲了原生命令,就会绕过依赖锁定;
- 仓库体积膨胀:依赖源码副本被提交进项目,仓库大小明显增加;
- 工具链行为不一致:IDE、CI、脚本等环境需要额外适配,容易出现「本地能编、CI 编不过」的情况。
godep 算是一个妥协的产物:它把问题解决了,但它毕竟更改了 go build 的原义,所以这个方案还是不够满意。
vendor 方案
所以 vendor 来了。它的核心是把依赖放置约定为项目内的一个特定目录,构建时优先使用项目内的依赖副本,从而在不改变 go build 语义的前提下实现依赖锁定。这是一个更自然的设计:锁定行为内建于工具链,而不是依赖外部包装命令。
如何启用
推广是一个渐进的过程,早期需要通过环境变量显式开启:
GO15VENDOREXPERIMENT=1
在 go1.5 中需要设置这个环境变量才能启用该特性,而到 go1.6 则默认开启,不再需要额外配置。
两种方案的对比
- 构建命令:godep 需要替换为标准命令的包装形式;vendor 直接使用
go build; - 依赖存放:godep 放在工具约定的 Godeps 目录;vendor 使用语言约定的依赖目录;
- 生态兼容:godep 需要各工具链额外支持;vendor 由官方工具链直接识别;
- 迁移成本:已有 godep 项目需要调整目录结构与构建脚本,但收益是长期的。
实践建议
- 把依赖内容纳入版本控制,确保任何人在任何时间检出代码都能直接构建;
- 统一构建入口,用脚本或 Makefile 封装构建过程,避免参数因人而异;
- 明确依赖更新流程,依赖升级应当是一次显式提交,而不是隐式跟随上游;
- 在干净环境验证构建,CI 中从零构建一次,确认依赖没有遗漏;
- 定期审视依赖,长期不升级会积累兼容与安全问题,需要安排固定节奏的更新窗口。
小结
从 godep 到 vendor 的演进,反映的是一个朴素的工程原则:构建结果必须可复现。当依赖是源码时,只有把依赖本身也纳入版本控制,才能保证「同一个提交,在任何机器上构建出同样的结果」。理解这段历史,也有助于理解后来各种依赖管理方案的设计意图。
| 问题根源 | Go 依赖以源码形式参与编译,上游 API 变更可能导致本地编译失败 |
|---|---|
| godep 做法 | 复制依赖源码到 Godeps 目录,并把 go build 换成包装命令 |
| godep 代价 | 改变 go build 语义、仓库体积增大、工具链适配成本高 |
| vendor 做法 | 依赖放入项目内约定目录,构建时优先使用,语言原生支持 |
| 启用方式 | go1.5 需设置 GO15VENDOREXPERIMENT=1,go1.6 起默认开启 |