Go的依赖管理工具
go语言与js和python类似, 依靠社区来丰富自己的组件库, 各种第三方库遍地开花. 因为go还是一门相当年轻的语言, 特性和接口还在不断更新, 加上社区活跃程度高, 就出现了这样的场景: 新的第三方库不断增加, 旧有的库的特性和接口也频繁变更.(这一点跟目前js社区一片热火朝天的景象很类似)
目录
历史存在问题
go一开始就有依赖管理的, 当我们在程序中需要import一个第三方库时, 通常我们是通过go get
指令:
go get github.com/org/libname
这种方式相对简单粗糙, 只能说是安装依赖而不能说是管理依赖. 一旦第三方库的接口做了不兼容改动(这在大版本更新很常见), 依赖这个库的程序后续很可能就无法通过编译, 或者出现执行时异常. 在go1.5版本之后, 官方就通过引入vendor来试图解决这个问题, 后来也推出了工具dep. 在官方解决这个问题之前, 社区出现了多个致力于解决依赖问题的工具, 比较出名的就包括govendor, glide, godep等.
在提到go依赖管理工具的设计之前, 先来看下python和js是如何解决这个问题的.
python的依赖管理
python的最常用的依赖管理工具是setuptools和pip. 对于打包的程序而言, 在setup.py里面指明依赖的库和版本号, 打包安装时会自动下载这些依赖. 对于非打包的程序, 常用easy_install
和pip
两个命令行程序. 在执行easy_install libname
或pip install libname
后, 工具将下载的第三方库代码存放到python安装目录下的site-packages
目录下, 这样python程序可以在任何地方导入这些第三方库. pip
做了更细致的操作, 它允许安装第三方库时指定版本号, 比如pip install libname==1.0.0
; 更进一步的, 它支持从文件中读取依赖列表, 通常pip install -r requirements.txt
就可以一键安装项目所需所有依赖.
js的依赖管理
js最流行使用npm来进行依赖管理. 程序的基本信息和依赖项列表都集中放置在packages.json文件中. 该文件指定程序的常规依赖, 开发环境依赖和”同伴依赖”(dependencies, devDependencies, peerDenpendencies)等, 并指定了版本号(一般而言, 其实是程序所需的最低版本号). 每一个使用npm包管理器的程序都如此操作, 最终得到一个依赖关系网. npm将这些依赖包下载并统一放置在程序目录下的node_modules目录下, 然后根据依赖关系创建软链.
npm这种”指定最低版本号”的做法无法避免依赖库更新后接口变更导致的问题, 因此后续出现了npm-freeze, npm shrinkwrap和yarn等工具, 除了依旧维护packages.json文件外, 还额外维护一个.lock文件, 指定了依赖的安装地址和详细版本以及最近提交的commit哈希. 这样程序在安装时就遵照这份清单去下载依赖.
官方工具dep
官方工具dep, 第一个释出版本是17年5月的0.1.0, 到18年1月释出版本0.4.1至今.
使用官方工具的优点是它是”官方”的, 后续的更新有保证, 并可能逐渐成为标准和唯一推荐的工具. 另外, 如果你继续往下看dep的使用, 你会爱上这个工具的.
需要明确的是, dep是设计给开发者使用的, 而不是给最终用户使用的. 最终用户安装go程序用的应该是go get.
安装
二进制安装:
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
MacOS
brew install dep
brew upgrade dep
Arch Linux
pacman -S dep
源码安装
下面的代码段从源代码安装最新版本的dep,并在二进制文件中设置版本,以便dep版本按预期工作。
go get -d -u github.com/golang/dep
cd $(go env GOPATH)/src/github.com/golang/dep
DEP_LATEST=$(git describe --abbrev=0 --tags)
git checkout $DEP_LATEST
go install -ldflags="-X main.version=$DEP_LATEST" ./cmd/dep
git checkout master
开发
如果想查看或修改dep源码, 直接go get就可以
go get -u github.com/golang/dep/cmd/dep
在新项目中使用
假设现在有个新的项目要开展, 并且目录将是
$GOPATH/src/github.com/me/example
现在, 创建并进入该目录并初始化dep
mkdir -p $GOPATH/src/github.com/me/example
cd $GOPATH/src/github.com/me/example
dep init
执行完可以看到当前目录下有两个新文件和一个vendor目录, 这样就初始化完毕了.
Gopkg.toml
Gopkg.lock
vendor/
基本上, 文件结构和命名方式跟npm相似度很高
此时可以加上git版本控制, 或者直接创建go文件开始编码, 又或者, 如果知道将要依赖某个第三方库, 也可以先安装它
dep ensure -add github.com/foo/bar github.com/baz/quux
常用操作
为了节省开发者的时间和精力, dep用了最简设计, 仅需掌握两个指令就能完全发挥dep的功能.
- dep ensure 最主要的指令, 也是唯一一个会修改文件状态的指令
- dep status 显示当前项目状态和依赖视图
dep的使用基本就是围绕dep ensure
. ensure这个词表达了这样的意思: 它将保证维护一种状态, 而不仅仅是执行一些离散的操作. 官方的说法是:
The verb is “ensure” to imply that the action is not just some single, discrete action (like adding a dependency), but enforcing some kind of broader guarantee. If we wanted to express the dep ensure guarantee as a sentence, it would go something like this:
Hey dep, please make sure that my project is in sync: that Gopkg.lock satisfies all the imports in my project, and all the rules in Gopkg.toml, and that vendor/ contains exactly what Gopkg.lock says it should.”
也就是说, 该指令将保证(正常情况下)
- 项目处于完全同步的状态
- .lock文件与项目实际导入的库和.toml的描述完全相符
- vendor目录也确实包含了.lock文件中指定要安装的库
并且官方也保证了, 在遇到意外情况时, 这个指令的操作结果也是可以预料到的. 这些意外情况包括:
- 指令执行到一半被kill
- 机器断电
- 其他致命错误
也就是说该指令要么完全成功, 要么完全不成功(不成功将会报错). 在不成功的状态下, 硬盘上的文件状态相较之前都不会有变化, 不会存在一个中间状态, 不会产生污染. 换句话说, 这个指令是事务的, 一致的.
在四种情况下我们会用dep ensure
- 新增一个依赖
- 更新现有依赖
- 捕捉项目代码中首次导入的依赖, 或移除不再需要的依赖
- 捕捉Gopkg.toml中的规则变更
当然还有第五种情况: 当不确定上述情况之一是否已经发生时:). 总之dep ensure
能让我们的项目处于正确的状态下并且无论何时执行总是安全的. 总之就是要多用, 不要慌.(在项目任何子目录下执行也都可以)
好了, 具体对应到上述几种情况, 操作方式分别是
新增依赖
比如需要依赖于github.com/pkg/erros, 则直接执行下方指令即可
dep ensure -add github.com/pkg/errors
上述指令将会更新Gopkg.lock
文件和vendor
目录, 并将github.com/pkg/errors
的最佳猜测版本约束注入我们的Gopkg.toml
文件中. 不过, 该指令也可能会提示如下错误
"github.com/pkg/errors" is not imported by your project, and has been temporarily added to Gopkg.lock and vendor/.
If you run "dep ensure" again before actually importing it, it will disappear from Gopkg.lock and vendor/.
正如报错信息所说, 我们应该尽快在代码中加入import github.com/pkg/errors
, 以防止下次执行dep ensure
时dep将该依赖标记为”未使用”而自动将其移除出vendor
和Gopkg.lock
(注意, 不包括.toml). 这也意味着, 如果我们需要一次添加多个依赖, 我们应该在一条指令中搞定, 而不是一个一个来
dep ensure -add github.com/pkg/errors github.com/foo/bar
之所以会这样是因为dep会静态分析项目代码以确认哪些依赖是真实必须存在的. 这个设计思想与”无用的依赖不要导入”是一致的.
如果觉得麻烦, 也可以不用这个dep ensure -add
指令. 直接在代码中import然后执行dep ensure
就好了. 不过在这种情况下, 最好检查一下.toml文件内容(依赖项的限制条件)是否如意料一样.
更新依赖
更新某个依赖可以执行
dep ensure -update github.com/foo/bar
也可以不带参数更新所有依赖, 但是不怎么推荐, 尤其是依赖项很多而且项目稍微有点历史的情况下.
dep ensure -update
根据Gopkg.toml里对依赖项的限制条件不同, 执行update指令后, 某些依赖能主动进行更新, 有些可能就需要修改.toml了. 如果想知道有哪些依赖项将会更新, 可以执行dep status
查看LATEST
. 如果想知道具体这些限制条件有何区别, 可以参考官方指引
第三方工具govendor
第三方工具govendor不得不提, 因为很多开源项目使用govendor进行依赖管理, 如需分析源码或贡献代码的话就需要掌握其使用.
govendor是go发展了vendor后出现的, 因此该工具也将依赖包的代码存放在vendor目录下, 并根据vendor/vendor.json文件进行管理.
在govendor眼中, 项目中的包有以下几种状态(第二列括号内字母为缩写):
+local (l) 项目本身的包
+external (e) 外部包(已在$GOPATH中)
+vendor (v) 放在vendor目录下的包
+std (s) 标准库里的包
+excluded (x) vendor指定要排除的外部包
+unused (u) vendor目录中的无用包
+missing (m) 缺失(无法找到, 还没下载)的外部包
+program (p) 该包可执行的程序
+outside 外部包, 包含已下载(到$GOPATH)和未下载
+all 所有包
这些状态词对应一类包集合, 比如可以通过如下指令列出所有外部包
govendor list +external
安装
go get -u github.com/kardianos/govendor
指令详情
init 创建vendor目录和vendor.json文件
list 列出当前项目的依赖和包
add 从$GOPATH中添加包(到vendor)
update 从$GOPATH中更新包(到vendor)
remove 从vendor目录中移除包
status 列出包状态(缺失, 过期, 已修改等等)
fetch 从远程仓库中拉取或更新包到vendor
sync 根据vendor.json文件变更从远程仓库同步vendor目录
migrate Move packages from a legacy tool to the vendor folder with metadata.
get 作用类似"go get"但是将依赖包放进vendor目录中
license List discovered licenses for the given status or import paths.
shell Run a "shell" to make multiple sub-commands more efficient for large
projects.
go tool commands that are wrapped:
`+<status>` package selection may be used with them
fmt, build, install, clean, test, vet, generate, tool
操作
govendor也需要一个初始化的过程,
cd $GOPATH/src/myname/myapp
govendor init
govendor init
指令创建了vendor目录, 并在该目录下创建文件vendor.json
然后, 如果你的项目已经有导入第三方库, 则执行
govendor add -e # 将已导入的$GOPATH中的第三方包拷贝到vendor目录
然后可以执行govendor list
查看各类包的情况, 或者govendor list -v packageName
查看某个包.
如果需要新增一个依赖, 可以指定该依赖的包版本
govendor fetch golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55 # 抓取截止到某个提交的代码
govendor fetch golang.org/x/net/context@v1 # 抓取取名为v1的分支或tag的最新版本, 如v1.1.2
govendor fetch golang.org/x/net/context@=v1 # 抓取名为v1的分支或tag
如需更新依赖, 则指令与新增的基本一致, 只需要去掉版本限制
govendor fetch golang.org/x/net/context
在项目开发前期, 如果没有用govendor而是直接go get, 那么可以继续用这种方式开发, 到项目结束时使用govendor add -e
将这些包拷贝到vendor目录即可, 拷贝的包不会包含.git目录.
在使用git时, 我们通常会将vendor目录内的包代码排除出版本控制, 但是又需要保留vendor/vendor.json
文件, 可以在.gitignore中添加
vendor/*/
其他开发者下载代码后, 只需执行
govendor sync
就可以将第三方依赖都下载下来了