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_installpip两个命令行程序. 在执行easy_install libnamepip 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

  1. 新增一个依赖
  2. 更新现有依赖
  3. 捕捉项目代码中首次导入的依赖, 或移除不再需要的依赖
  4. 捕捉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将该依赖标记为”未使用”而自动将其移除出vendorGopkg.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

就可以将第三方依赖都下载下来了