说实话,Golang对一个新人真的挺不友善的,因为一上手要了解的概念。你看人家Java,上来一个项目mvn install一下就完事了,赶紧利落。但是Golang就麻烦了,你得先了解什么是GOPATH。我当年刚接触Golang真正开始做项目的时候,只知道按要求配置环境变量,对GOPATH真正理解可能都是好几个月以后的事情了。说白了,还是因为懒。真正做项目的人,有多少有耐心砍柴磨刀,出现一个东西就研究半天啊,我们只是想要Copy-Paste而已。
但是不得不承认,对于今天讨论的Go包管理的话题,如果你想理解Golang的包管理机制,连GOPATH都不想充分理解一下,那可能真的不需要看这篇文章,下次遇到了照着README一步步老老实实来就行了。照着README文档搞不定怎么办?给项目维护者提BUG啊!
GOPATH
言归正传,还是回到GOPATH的理解上来。那么,GOPATH是什么?有什么用?本质上GOPATH是一个系统的环境变量,就是Go语言用来存放代码依赖的地方。
很多人搞不清 GOPATH 、 GOROOT 的区别,其实没必要理解的很复杂。当Go语言的安装包刚下载完毕的时候,你把它解压或者直接安装到的那个目录,就是 GOROOT 目录,此时你需要做一些额外的配置,将GOROOT这个环境变量设置一下。当然,对应的bin目录你也得设置一下,否则操作系统找不到go的执行文件。
比如你解压后安装到 /opt/go/ 目录下了,又或者在Windows下面你安装到了C盘 C:\\GO,都是OK的,区别只是不同操作系统环境下设置的方法不同而已。具体怎么设置我就不赘述了。
到此为止,你已经可以忘记 GOROOT 这个事情了,因为已经解决了所有跟它有关的事情。但是我们还是要解释下,为什么要设置这个东西?道理很简单,Golang的很多默认机制都很喜欢从环境变量里面去读内容,设置了 GOROOT 环境变量,相当于告诉所有读取这个变量的程序我们Golang的源码位置,便于代码的引用。可以理解为 GOROOT 就是三原色,用它可以组合出很多不同的色彩,是最初始的代码依赖。GOROOT 里面的很多代码都是系统驱动程序以及系统调用。
那么我们有了三原色,想要配出更多的颜色,我们调配颜色的过程中组合出的颜色,也就是新写出来的代码包放在哪里呢? 你一定已经猜到了,这就是 GOPATH 目录的作用。所有Golang安装包以外的代码,无论是你自己写的,还是第三方的无比成熟的包,都需要放置在GOPATH下面。
所以你不要再问为什么我直接 git clone 下来的代码怎么各种报错说找不到依赖啦,你设置 GOPATH 了吗?
那么怎么设置 GOPATH 呢? 这就根据个人口味定了,很随意。有的人选择用隐藏目录,比如 ~/.go/ 作为GOPATH,也有的人设置在GOROOT的隔壁,新建了个目录叫/opt/gopath,对于Windows用户来说也是如此,当然你完全可以设置环境变量把自己在自己的D盘下也创建一个叫gopath的目录D:\\Gopath,然后设置下环境变量。而我个人则更偏爱/home/sunjianbo/gopath这个目录,也就是~/gopath。
那么我可不可以有多个GOPATH目录呢?当然可以,设置系统环境变量的时候就是可以放多个值的嘛。
你完全可以设置 export GOPATH=~/gopath1:~/gopath2:~/gopath3。在配置的这些目录中,Go程序会依次去寻找有没有对应的依赖包。
所以是不是有的读者已经想明白最原始的包管理方法了呢?
公布答案,就是每个项目做一个 GOPATH。
具体而言,假设我们有个项目叫 tastego,我们在里面写个脚步,内容如下:
export GOPATH=$PWD/tastego:$GOPATH
只要一行,简单到任性。当然,最好再加一行,把GOPATH下的bin路径加上,这样go install出来的内容也能开箱即用。
export PATH=$PATH:$PWD/tastego/bin
对于每个项目的依赖,分门别类的放在对应的源码目录下。
tastego/ src/ github.com/ wonderflow/... qiniu/... golang.org/... bin/ pkg/
所以,你可能不信,最开始Go官方根本没有提供什么包管理机制的时候,好多Go语言玩家,都是用这样的方式在玩。照样玩的顺风顺水,数十上百万行代码不在话下。
但是值得一提的是,我们观察到 GOPATH=$PWD/logkit:$GOPATH 这个结构是一个递归的结构,不仅可以这么写,换个方式GOPATH=$GOPATH:$PWD/logkit 也是有效的。但是这两种写法的利弊是不同的。先说结论,这两种方式我都不推荐。
- GOPATH=$PWD/logkit:$GOPATH 这个写法是当前项目的目录在前,所以遇到新的项目,永远在gopath的最前面。这个很大的好处就是当你遇到多个包名称完全相同的时候,用的默认是最前面的包,所以包冲突的概率会降到最低。坏处也显而易见,你 go get 下来的代码包,会进入到最前面的这个目录,导致每次 go get的位置是不确定的,尤其是环境变量被无意中clean后,你甚至需要重新 go get一遍。
- GOPATH=$GOPATH:$PWD/logkit 这个写法就保证了顺序的稳定性,后来的包一定在后面,但是弊端就是你很快就要出现包版本冲突的问题了。
当然,上述做法还有一个巨大的缺点,就是当你的项目大了以后,你可能不得不把半个Github的代码都放在你的单个代码库(Repo)下面。
百花齐放
终于,在Golang官方面对GOPATH管理的各种乱象始终无动于衷的时候,社区看不下去了,相继出现各类包管理工具。
2013年的时候,大名鼎鼎的Gedep工具诞生了。这是社区第一个包管理工具,受到了大家的热烈追捧。原理非常简单,就是把我们上一节讲的方式,通过脚本来实现。把所有的源码保存到一个叫 Godeps/_workspace/ 的目录下,然后将这个Godeps/_workspace/ 目录作为唯一的 GOPATH. 提供了一系列包括 godep save 、 godep restore 这样方便快捷的把所有依赖包都保存到 Godeps/_workspace/ 目录下,同时又可以快速的通过godep 恢复 GOPATH,相当于对GOPATH及源码的版本做了一次快照。当然,它并没有解决你一次git clone需要下载半个github的难题。
但是就在这一年,Docker开始火起来了,所以好多Golang项目构建Dockerfile做测试的起手式就是:
ADD . /app/xxx/ ENV GOPATH /app/xxx/Godep/_workspace
一下子所有依赖全都有了,确实很方便。
但是在当时(2013~2014年),这个工具依然没有解决我的版本问题,依然没有解决我依赖的包如果还依赖许多别的包怎么办的问题。于是到了2014年,号称 参照了其他语言的包管理工具**最佳实践** 的 glide 包管理工具诞生了。
那么它到底凭什么号称自己是最佳实践呢?它的最佳体现在它的包管理哲学上,一旦你安装完成glide以后,一个glide create命令,就会完全搞定所有包依赖的问题。至于说构建时有多个版本可以用,依赖怎么选、选哪个的问题,它会帮你搞定。
那如果我希望自己指定包版本呢?当然也没问题,只要在 glide.yaml 配置文件中填写好你对应的包版本、或者版本的范围,glide 会在这个范围内选定。
所以对于从零构建项目的新用户来说,glide 一定是个不错的选择,它帮你处理了所有的包依赖问题。但是对于一个已经有一些历史包袱的项目来说,使用 glide 可能就有些尴尬了。尤其是你的项目里本来就存在一些依赖冲突的问题,glide 还非要帮你确定使用哪个依赖,导致最终还是运行不起来。
所以有人就问了,“可不可以不要绝对的给我最佳实践,把我的依赖全导进来,一步步来行么?” 官方就说,"我们的设计哲学是glide管理下的每一步都是可执行的,不能只走半步!" 但是您没给人解决问题啊,最终还是执行不了,这就很僵……
也许正是理念的不同,所以 glide 号称“最佳实践”但却并未一统江湖。
这段时间还有诸如 gopkg.in、gom 等包管理工具诞生,其理念都比最初的godep先进,你说与 glide 比没有更多特色,都加入了包版本的概念。
Vendor
时间走到了2015年,Golang官方终于看不下去了,在推出go1.5版本的同时,首次实验性质的加入了 vendor机制 功能。当然,这个功能毕竟是实验性质的,默认情况下是关闭的,导致大多数用户实际上根本不会用它。直到2016年,在官方推出go1.6版本的时候,vendor机制 才默认变成开启的状态。
那么到底什么是 vendor机制 呢? 通俗的说,就是在你的项目中包含了一个vendor文件夹,go语言会把它默认为一个GOPATH。于是,你就可以在里面放你的依赖库啦。
举例来说,假设你托管在github上的项目是这样的,项目名称为tastego:
tastego/ main.go common/ common.go util/ util.go
其中除了 main.go 主函数以外,还包含2个自己写的库(package),一个是common,一个是util,那么为了让项目可以正常编译,这2个库应该在GOPATH中,那么实际上在GOPATH的结构下,你的项目目录是这样的(wonderflow是我的github ID):
$GOPATH/ src/ github.com/ wonderflow/ tastego/ main.go common/ common.go util/ util.go
然后加入了vendor机制后,你的项目目录下增加一个vendor的文件夹,里面可以放别的依赖,形式上就是:
$GOPATH/ src/ github.com/ wonderflow/ tastego/ main.go common/ common.go util/ util.go vendor/ github.com/ qiniu/ ... golang.org/ ...
让我们再回顾一下本文刚开始描述的基于GOPATH的包管理方法:
tastego/ src/ github.com/ wonderflow/... qiniu/... golang.org/... bin/ pkg/
看上去非常相似,只是有了vendor,就有了官方的正名!并且你再也不需要手工(半手工)修改GOPATH,项目的形态也跟以前的统一起来了,好处显而易见。
但是问题就真的解决了吗?实际上并没有全部解决问题,反而由于在随后2016、2017年,vendor机制成为正式的Go规则,问题日益严重。
- 嵌套的vendor目录问题:vendor目录下面的项目里面的vendor目录怎么办?
- vendor机制本身没有版本概念,不同版本间类型不兼容问题依旧存在。
- 与其他 GOPATH 下的包init函数冲突问题:出现了相同的包,重复的init() 函数又怎么办?
所以Golang团队成员也召开了大会,非常赞同社区里各种包管理工具的理念,确实有必要对包管理提出一个统一的规则,来解决上面的问题。但是问题不是没有规则,而是规则太多了。往往就是一个意见不合,一下子就杀出来一个新的工具。仅官方推荐的包管理工具就有15种之多。
- dep
- manul - Vendor packages using git submodules.
- Godep
- Govendor
- godm
- vexp
- gv
- gvt - Recursively retrieve and vendor packages.
- govend - Manage dependencies like go get but for /vendor.
- Glide - Manage packages like composer, npm, bundler, or other languages.
- Vendetta
- trash
- gsv
- gom
- Rubigo - Golang vendor utility and package manager
具体的可以参见官方的wiki页面: https://github.com/golang/go/wiki/PackageManagementTools
所以Go官方也开始尝试把包管理做成 go tools 工具链中的一个,官方的包管理工具就是 dep 但是目前这个项目还不成熟,还没有纳入到工具链中。
但是官方的建议已经很明显了,让大家尽量使用包管理工具去引入依赖,当然最好是尽量使用标准库;另一方面则是尽量使用现有的包管理工具,而不是自己再去造一套规则。
所以,我们也来学习一下包管理工具该怎么用,经过多次对比调研,笔者推荐
Govendor 工具,所以也以之为例介绍。
Govendor
Govendor 本质上就是把源码拷贝到vendor目录下,通过在vendor目录维护一个 vendor.json 的文件,指定使用的包版本。整个目录结构清晰,在同步到github时,既可以把代码直接全部包含到项目中,也可以用 .gitignore 忽略依赖的库并通过 govendor sync 同步。
安装
安装就跟所有golang的工具一样,go get 即可.
go get -u github.com/kardianos/govendor
初始化项目
对于一个现有的项目,没有使用过任何包管理工具的话,开始使用Govendor 非常简单。
进到项目目录下,执行初始化:
cd "my project in GOPATH" govendor init
将现有依赖加入到当前项目的 vendor 中管理。
govendor add +external
此时,你已经顺利将现有项目切换到govendor管理了。
项目过程中的常用命令
初始化项目完毕后,就到了项目常规管理阶段,通常情况下,会有下列这些场景的需求。
添加依赖
如果你本地GOPATH中已经存在,使用 govendor add
# 指定版本的commit,包名后跟 @ 符号加上 commit ID govendor add golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55 # 指定版本名称,包名后跟 @ 符号加上版本名称或分支名称 govendor add golang.org/x/net/context@v1 # Get latest v1.*.* tag or branch.
如果你本地不存在,使用 govendor fetch, 其他指定版本的方式与 govendor add 相同
一次性把所有项目的依赖库全加载进来,就是我们初始化时介绍的命令。
govendor add +external
移除依赖
移除一个依赖
govendor remove golang.org/x/net/context
移除所有项目已经不用的依赖
govendor remove +unused
更新依赖
当然你可以选择 govendor remove ,然后再 govendor add。
你还可以直接使用 govendor update 更新本地GOPATH中已经更新的包。
若本地不是最新的或不存在,请用 govendor fetch 更新。
govendor update golang.org/x/net/context@a4bbce9fcae005b22ae5443f6af064d80a6f5a55
同步govenodr包
一般情况下,开源项目的协作过程中,其他人更新了项目的govendor,那么你也要同步过来,直接使用下面的命令即可。
govendor sync
查看本地依赖以及包状态
通常情况下拿到一个项目可能会想要直观的了解他有哪些依赖关系,使用 govendor list即可查看。
纯粹使用 govendor list 价值不大。
有意思的是,Govendor给Golang的依赖包加入了状态描述,结合各类vendor的状态参数进行各类操作就很有意思。
状态一共有如下几类:
+local (l) 仅存在项目中的包 +external (e) 在GOPATH中有,但是项目中没有的包 +vendor (v) 在项目vendor目录中的包 +std (s) 使用golang标准库 +excluded (x) 项目中不包含且明确申明要排除在外的包 +unused (u) 在vendor目录中,但实际项目没有用到的包 +missing (m) 项目中用到但是找不到的包(此时需要govendor fetch获取) +program (p) 带有main函数入口的包 +outside 所有外部包组合, 包括 (+external +missing) +all 列出所有的包
这些状态信息可以与其他命令连用,比如
govendor add +external
govendor remove +unused
最酷的是,状态还可以做逻辑 与 以及 或 的操作,比如:
+local,program (local AND program) 表示项目中的包同时又是主函数入口 +local +vendor (local OR vendor) 表示项目中的包以及vendor中的包 +vendor,program +std ((vendor AND program) OR std) 表示vendor中的包同时带有主函数入口,再加上标准库的包 +vendor,^program (vendor AND NOT program) 表示vendor目录中的包,但是不包含有主函数入口的包
查看包之间的依赖关系
使用 -v 参数可以查看一个包被哪些包依赖:
govendor list -v
那么反过来,你可能想知道一个包依赖了哪些包?这个是go工具链里面提供的方法,直接使用 go list
比如:
go list -f '{{ .Imports }}' github.com/wonderflow/tastego
查看包的实际路径
通过 -p 参数可以看到包所在的实际文件路径,区别于import时填写的路径,实际路径可以快速找到你引用的包位置。
查看所有当前项目的包:
govendor list -p -no-status +local
至此,我们,Govendor的常用命令已经介绍完了,相信掌握了这些,在今后的项目中管理各种依赖包,你一定游刃有余。
感谢徐川对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889