当开始真正用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状态。
下面来看看使用的比较多的gode, 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"
}
]
}
如果要增加新的依赖包:
- Run go get foo/bar
- Edit your code to import foo/bar.
- Run godep save (or godep save ./…).
如果要更新依赖包:
- Run go get -u foo/bar
- 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
拷贝点代码怎么了,对吧。