当开始真正用go去做项目的时候,不可避免的就会遇到依赖包的问题。go的依赖包管理与java+maven的依赖管理不太一样,我们从GOPATH开始逐渐理解其思想,然后再对比下目前常用的依赖包管理工具。

GOPATH和GOROOT

初学者很容易会被这两个环境变量给搞晕。

GOROOT并不是必须要设置的。

GOROOT不是必须要设置的。参见Installing to a custom location,默认go会安装在/usr/local/go下,但也允许自定义安装位置,GOROOT的目的就是告知go当前的安装位置,编译的时候从GOROOT去找SDK的system libariry。

例如我用的是ubuntu 16.04,默认go的版本是1.6,如果我想升级为更新的版本,就需要自定义安装,所以我设置了GOROOT:export GOROOT=$HOME/go1.7.4

GOPATH必须要设置,但并不是固定不变的

GOPATH的目的是为了告知go,需要代码的时候,去哪里查找。注意这里的代码,包括本项目和引用外部项目的代码。GOPATH可以随着项目的不同而重新设置。

GOPATH下会有3个目录:src, bin, pkg。

  • src目录:go编译时查找代码的地方
  • bin目录:go get godep这种bin工具的时候,二进制文件下载的目的地
  • pkg目录:编译生成的lib文件存储的地方。

本项目内部的依赖

以kubernetes为例。kubernetes/cmd/kubectl/kubectl.go中引用了app包中的kubectl.go,代码是这样写的:

import (
	"os"
	"k8s.io/kubernetes/cmd/kubectl/app"
)

那么go在编译的时候怎么查找这个包呢?

这就是GOPATH发挥作用的时候了。go编译时会去$GOPATH/src/目录去查找需要的代码,因此只要上面app/kubectl.go在$GOPATH/src/k8s.io/kubernetes/cmd/kubectl/里面,go编译的时候就能找到,那么自然的,kubernetes/cmd/kubectl/kubectl.go也需要放到$GOPATH/src/k8s.io/里去。最终$GOPATH里的代码结构是这样的:

├── src
│   ├── k8s.io
│   │   └── kubernetes
│   │       ├── cmd
│   │       │   ├── kubectl
│   │       │   │   ├── app
│   │       │   │   │   ├── BUILD
│   │       │   │   │   └── kubectl.go
│   │       │   │   ├── BUILD
│   │       │   │   ├── kubectl.go
│   │       │   │   └── OWNERS

管理外部的依赖包

不可避免的我们会使用外部的依赖包包。go在这方面做的非常飘逸。go没有像java使用maven来管理依赖包、包版本,而是直接使用GOPATH来管理外部依赖。

使用GOPATH来管理外部依赖

go允许import不同代码库的代码,例如github.com, k8s.io, golang.org等等;对于需要import的代码,可以使用 go get 命令取下来放到GOPATH对应的目录中去。例如go get github.com/silenceshell/hcache,会下载到$GOPATH/src/github.com/silenceshell/hcache中去,当其他项目在import github.com/silenceshell/hcache的时候也就能找到对应的代码了。

看到这里也就明白了,对于go来说,其实并不care你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。

vendor

依赖GOPATH来解决go import有个很严重的问题:如果项目依赖的包做了修改,或者干脆删掉了,会影响我的项目。因此在1.5版本以前,为了规避这个问题,通常会将当前使用的依赖包拷贝出来。

为了能让项目继续使用这些依赖包,有这么几个办法:

  • 将依赖包拷贝到项目源码树中,然后修改import
  • 将依赖包拷贝到项目源码树中,然后修改GOPATH
  • 在某个文件中记录依赖包的版本,然后将GOPATH中的依赖包更新到对应的版本(因为依赖包实际是个git库,可以切换版本)

go作为一个现代化的语言,居然要用这么复杂不直观而又不标准的方法来管理依赖,难怪在早期会有很多人非常不看好go的前景。

为了解决这个问题,go在1.5版本引入了vendor属性(默认关闭,需要设置go环境变量GO15VENDOREXPERIMENT=1),并在1.6版本中默认开启了vendor属性。

简单来说,vendor属性就是让go编译时,优先从项目源码树根目录下的vendor目录查找代码(可以理解为切了一次GOPATH),如果vendor中有,则不再去GOPATH中去查找。

kube-keepalived-vip为例。该项目会调用k8s.io/kubernetes的库(Client),但如果你用1.5版本的kubernetes代码来编译keepalived,会编译不过:

./controller.go:107: undefined: "k8s.io/kubernetes/pkg/client/unversioned".Client

查下代码会发现1.5版本中代码有变化,已经没有这个Client了。这就是前面说的依赖GOPATH来解决go import所带来的问题,代码不对上了。

kube-keepalived-vip项目用vendor目录解决了这个问题:该项目把所有依赖的包都拷贝到了vendor目录下,对于需要编译该项目的人来说,只要把代码从github上clone到$GOPATH/src以后,就可以进去go build了(注意,必须将kube-keepalived-vip项目拷贝到$GOPATH/src目录中,否则go会无视vendor目录,仍然去$GOPATH/src中去找依赖包)。

但是vendor目录又带来了一些新的问题:

  • vendor目录中依赖包没有版本信息。这样依赖包脱离了版本管理,对于升级、问题追溯,会有点困难。
  • 如何方便的得到本项目依赖了哪些包,并方便的将其拷贝到vendor目录下? Manual is fxxk.

社区为了解决这些(工程)问题,在vendor基础上开发了多个管理工具,比较常用的有godep, govendor, glide。go官方也在开发官方dep,目前还是Alpha状态。

下面来看看使用的比较多的godep, glide和 govendor。

godep

godep的使用者众多,如docker,kubernetes, coreos等go项目很多都是使用godep来管理其依赖,当然原因可能是早期也没的工具可选。

godep早期版本并不依赖vendor,所以对go的版本要求很松,go 1.5之前的版本也可以用,只是行为上有所不同。在vendor推出以后,godep也改为使用vendor了。

godep使用很简单:当你的项目编写好了,使用GOPATH的依赖包测试ok了的时候,执行:

$ godep save

hcache为例,执行go save,会做2件事:

  • 扫描本项目的代码,将hcache项目依赖的包及该包的版本号(即git commit)记录到Godeps/Godeps.json文件中
  • 将依赖的代码从GOPATH/src中copy到vendor目录(忽略原始代码的.git目录)。对于不支持vendor的早期版本,则会拷贝到Godeps/_workspace/里

一个Godeps.json的例子。

{
    "ImportPath": "github.com/silenceshell/hcache",
    "GoVersion": "go1.7",
    "GodepVersion": "v79",
    "Deps": [
        {   
            "ImportPath": "github.com/tobert/pcstat",
            "Rev": "91a7346e5b462a61e876c0574cb1ba331a6a5ac5"
        },  
        {   
            "ImportPath": "golang.org/x/sys/unix",
            "Rev": "0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79"
        }   
    ]   
}

如果要增加新的依赖包:

  1. Run go get foo/bar
  2. Edit your code to import foo/bar.
  3. Run godep save (or godep save ./…).

如果要更新依赖包:

  1. Run go get -u foo/bar
  2. Run godep update foo/bar. (You can use the … wildcard, for example godep update foo/…).

godep还支持godep restore,可以将vendor下的代码反向拷贝到$GOPATH下。不过我没想到这个功能在什么情况下可以用到。

glide

glide也是在vendor之后出来的。glide的依赖包信息在glide.yaml和glide.lock中,前者记录了所有依赖的包,后者记录了依赖包的版本信息(合成一个多好)。

glide使用也不麻烦:

glide create  # 创建glide工程,生成glide.yaml
glide install # 生成glide.lock,并拷贝依赖包
work, work, work
glide update  # 更新依赖包信息,更新glide.lock

glide install会根据glide.lock来更新包的信息,如果没有则会走一把glide update生成glide.lock

最终一个使用glide管理依赖的的工程会是这样:

──$GOPATH/src/myProject (Your project)
  ├─ glide.yaml
  ├─ glide.lock
  ├─ main.go (Your main go code can live here)
  ├─ mySubpackage (You can create your own subpackages, too)
  |    ├─ foo.go
  ├─ vendor
       ├─ github.com
            ├─ Masterminds
                  ├─ ... etc.

glide的功能更丰富一些。

  • glide tree可以很直观的看到vendor中的依赖包(以后会被移除掉,感觉没啥用)
  • glide list可以列出vendor下所有包
  • glide支持的Version Control Systems更多,除了支持git,还支持 SVN, Mercurial (Hg), Bzr
  • 最重要的,glide.yaml可以指定更多信息,例如依赖包的tag、repo、本package的os, arch。允许指定repo可以解决package名不变,但使用的是fork出来的的工程

govendor

govendor是在vendor之后出来的,功能相对godep多一点,不过就核心问题的解决来说基本是一样的。govendor生成vendor目录的时候需要2条命令:

  • govendor init生成vendor/vendor.json,此时文件中只有本项目的信息
  • govendor add +external更新vendor/vendor.json,并拷贝GOPATH下的代码到vendor目录中。

govendor还可以直接指定依赖包版本来获取包,这也有了点版本管理的影子了。

# Setup your project.
cd "my project in GOPATH"
govendor init

# Add existing GOPATH files to vendor.
govendor add +external

# View your work.
govendor list

# Look at what is using a package
govendor list -v fmt

# Specify a specific version or revision to fetch
govendor fetch golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55
govendor fetch golang.org/x/net/context@v1   # Get latest v1.*.* tag or branch.
govendor fetch golang.org/x/net/context@=v1  # Get the tag or branch named "v1".

相比godep来说,govendor略繁琐一点(比如govendor init有啥用),功能上略丰富一些。

golang官方dep

虽说golang的dep还是alpha状态,但也可以用了。

$ dep init
$ dep ensure -update
$ dep ensure github.com/pkg/errors@^0.8.0

没啥特殊要求的话,一条dep init就够用了;如果要升级,或者指定某个tag版本,可以用dep ensure。dep还有个很好用的功能,dep prune,可以删除没有用到的package。

各依赖管理工具对比

go官方wiki给了一个比较全面的对比

godep更直观,使用者也多一些,一些个人的小项目可以用;glide功能更丰富,更接近maven(例如glide.lock跟maven的pom.xml比较类似,可以指定获取某一个版本),新的项目可以考虑使用glide。

当然了,还是期待golang的dep能够更好用,解决目前依赖包管理工具碎片化过多的问题。

gvt

glide/godep/govendor都只会拉import的依赖包,对于依赖包的依赖包则不会管。这种情况可以用gvt把所有的依赖全部拉到vendor目录下。是不是很酸爽,但对于解决golang.org库无法访问的问题还是很有帮助的,并且也可以拉平团队使用的所有依赖包。

vendor的问题

总的来说glide比较完善了。不过还是有些不太愉快的地方,如代码copy的泛滥:某个包在不同的项目中各有一份copy,而且其版本可能不一样;当依赖的包比较多的时候,vendor目录也会非常庞大。这是vendor的锅(或者说GOPATH的原罪,不能像maven一样对同一个包在本地有多个版本,本地只能有一份代码),似乎没办法避免。

当然了,也别指望go会改善这一点。

“Through the design of the standard library, great effort was spent on controlling dependencies. It can be better to copy a little code than to pull in a big library for one function. Dependency hygiene trumps code reuse.” - Go at Google

拷贝点代码怎么了,对吧。

gopkg.in

托管在github上的代码,并没有版本的概念,但是有branch/tag的概念。

然而不管是用godep, glide还是govendor,都希望依赖包有版本,这样只要固定依赖包的某一个版本,既可以及时更新依赖包的新特性,又不会因为依赖包接口更新导致不兼容的问题(接口更新一定会有大版本更新)。

gopkg.in可以为github上的包提供“版本”管理。例如gopkg.in/user/pkg.v3,对应的是github.com/user/pkg库的branch/tag v3, v3.N, or v3.N.M

gopkg.in/pkg.v3      → github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M)
gopkg.in/user/pkg.v3 → github.com/user/pkg   (branch/tag v3, v3.N, or v3.N.M)

gopkg.in是最长匹配的:

  • pkg.v1 → tag or branch v1
  • pkg.v2 → tag or branch v2.1.2
  • pkg.v3 → tag or branch v3.0

例如我的netproc库: github.com/silenceshell/netproc,只要我拉一个v1.0的branch即可:

git branch v1.0
git checkout v1.0
git add .
git push --set-upstream origin v1.0.0

之后就可以go get gopkg.in/silenceshell/netproc.v1的方式,来下载对应版本的包了。下载的代码会放在$GOPATH/src/gopkg.in/silenceshell/netproc.v1里。

不需要在gopkg.in上做任何操作!

感觉还是挺神奇的。大致走读了下gopkg的代码,当go get gopkg.in/{user}/{repo}.{version}的时候,gopkg会去github上查询对应github.com/{user}/{repo}的信息,如果有对应version的branch,则做一次proxy,go get下载的还是github上的代码。

gopkg.in目前只支持github.com。另外import的时候,只能v1, v2,不能v1.1。