Go的包管理工具(一)

13,749 阅读6分钟

在前面转载了系列文章:Golang 需要避免踩的 50 个坑,总得来说阅读量都挺大。今天这篇文章,咱们一起聊聊Go的依赖包管理工具。

背景

每一门语言都有其依赖的生态,当我们使用Java语言的时候,使用Maven或者Gradle管理包依赖。早期的Go被很多开发者所诟病的一个问题就是依赖包的管理。Golang 1.5 release版本的发布之前,只能通过设置多个GOPATH的方式来解决这个问题,例如:我两个工程都依赖了Beego,但A工程依赖的是Beego 1.1,B工程依赖的是Beego 1.7,我必须设置两个GOPATH来区分,并且在切换工程的时候GOPATH也得切换,无比痛苦。在Golang 1.5 release 开始支持除了GOROOTGOPATH之外的依赖管理:vender,官方 wiki 推荐了多种支持这种特性的包管理工具,如:Godep、gv、gvt、glide、govendor和官方的dep等。

环境准备

安装Go

笔者是Mac系统,安装Go有多种方式,通过brew、下载源码安装go等方式可以安装go。

在bash_profile中自定义GOPATH和GOBIN位置:

GOROOT=/usr/local/go
export GOPATH=/Users/user/aoho/go-workspace
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN:$GOROOT/bin

安装完成之后,查看go的环境变量:go env

GOARCH="amd64"
GOBIN="/usr/local/go/bin/go"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/user/aoho/go-workspace"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/st/gkm45qzd2tv8mc32my38_n_00000gp/T/go-build646095787=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

go的版本为:go version go1.9.3 darwin/amd64

GOPATH和GOROOT

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

如上面展示的结果,笔者使用的就是默认的安装地址,也可以通过 export GOROOT=$HOME/go1.9.3指定。

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

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

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

包管理

上面小节提到,依赖的代码去$GOPATH指定的位置寻找,这部分代码可能是本项目或者外部引用的项目。下面依次介绍这两种情况。

内部依赖管理

如笔者示例route_auth.go的引入:

import (
	"gwp/Chapter_2_Go_ChitChat/chitchat/data"
	"net/http"
)

route_auth.go需要引用data/user.go,项目结构如下:

编译时会去$GOPATH/src/目录去查找需要的代码,因此只要上面data/user.go在$GOPATH/src/gwp/Chapter_2_Go_ChitChat/chitchat/data里面,go编译的时候就能找到。

外部依赖管理

对于外部依赖的管理,在最开始go没有像java使用maven来管理依赖包、包版本,而是直接使用GOPATH来管理外部依赖。

GOPATH来管理外部依赖

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

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

  • 能拉取源码的平台很有限,绝大多数依赖的是 github.com
  • 不能区分版本,以至于令开发者以最后一项包名作为版本划分
  • 依赖 列表/关系 无法持久化到本地,需要找出所有依赖包然后一个个 go get
  • 只能依赖本地全局仓库(GOPATH/GOROOT),无法将库放置于局部仓库($PROJECT_HOME/vendor)

vendor

依赖GOPATH来解决go import存在的问题在上面小节已经列举。为了解决这个问题,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,会编译不过。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目录下?依靠人工实在不现实。

为了解决这些问题,开源社区在vendor基础上开发了多个管理工具,比较常用的有godep、govendor glide等,go官方发布了dep。

godep

godep是解决包依赖的管理工具,原理是扫描记录版本控制的信息,并在go命令前加壳来做到依赖管理。godep早期版本并不依赖vendor,所以对go的版本要求很松,go 1.5之前的版本也可以用,只是行为上有所不同。在vendor推出以后,godep也改为使用vendor了。godep 建议在 golang 1.6 以后使用,且godep 依赖 vendor 。

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

go get -u -v github.com/tools/godep

通过如上的命令安装,成功安装后,在$GOPATH的bin目录下会有一个godep可执行的二进制文件,后面执行的命令都是用这个,建议这个目录加入到全局环境变量中。

编译运行

因为go命令是直接到GOPATH目录下去找第三方库,且在1.6以后支持vendor方式编译,而使用godep下载的依赖库放到Godeps/workspace目录下的,但是不影响继续使用依赖GOPATH目录,所以与三方工具本身不冲突。因此使用:

godep go build main.go

godep中的go命令,就是将原先的go命令加了一层壳,执行godep go的时候,会将当前项目的workspace目录加入GOPATH变量中。

检出依赖

如果要增加新的依赖包:

  • 运行 go get github.com/globalsign/mgo
  • 代码中 import github.com/globalsign/mgo

项目编写好了,使用GOPATH的依赖包测试ok了的时候,执行:

godep save

如上的命令将会自动扫描当前目录所属包中import的所有外部依赖库(非系统库),并将所有的依赖库下来下来到当前工程中,产生文件 Godeps/Godeps.json 文件。

godep save时godep把所有依赖包代码从GOPATH路径拷贝到Godeps目录下,并去除代码管理目录。这个用处主要是为了支撑godep go tool的一系列操作,尤其是git clone了代码库下来后,通常直接用godep go install xxx即可完成编译,一定程度上能够缓解golang比较严格的代码路径和包管理带来的烦恼。在没有 Godeps 文件的情况下,生成模组依赖目录vendor文件夹。如果是开发依赖使用三方库,需要固定使用某个版本,请完全提交Godeps和vendor文件夹。

依赖包会有更新,如何更新依赖包?可以通过如下的命令实现。

  • 运行 go get -u github.com/globalsign/mgo
  • 运行 godep update github.com/globalsign/mgo
拉取依赖 restore

通过命令 godep restore同步依赖库,如果下载的项目中只有Godeps.json文件,而没有包含第三库则可以使用godep restore这个命令将所有的依赖库下来到$GOPATH\src中用于开发。

godep restore执行时,godep会按照Godeps/Godeps.json内列表,依次执行go get -d -v来下载对应依赖包到GOPATH路径下。

govendor

govendor是在vendor之后出来的,功能相对godep多一点,不过就核心问题的解决来说基本是一样的。该工具将项目依赖的外部包拷贝到项目下的 vendor 目录下,并通过 vendor.json 文件来记录依赖包的版本,方便用户使用相对稳定的依赖。

go get -u github.com/kardianos/govendor

如上的命令即可安装govendor,govendor生成vendor目录的时候需要2条命令:

  • govendor init生成vendor/vendor.json,此时文件中只有本项目的信息
  • govendor add +external更新vendor/vendor.json,并拷贝GOPATH下的代码到vendor目录中。 govendor还可以直接指定依赖包版本来获取包。

govendor的依赖包主要有以下多种类型:

使用步骤

进入项目的根目录。

# 创建 vendor 文件夹和 vendor.json 文件
govendor init

# 从 $GOPATH 中添加依赖包,会加到 vendor.json
govendor add +external

# 列出已经存在的依赖包
govendor list

# 找出使用的对应包
govendor list -v fmt

# 拉取指定版本的包
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".

相对上面的工具来说,govendor功能更加丰富。

总结

本文主要介绍了几种go依赖包管理工具,首先介绍了go的环境安装,配置对应的环境变量;其次讲到包管理的两种类型:内部依赖和外部依赖的管理。内部依赖包的管理很简单,go原生的外部依赖包管理存在很多缺陷,随后介绍了开源社区推出的godep和govendor,在vendor基础上进行了功能的完善。还有目前常用的包依赖管理工具glide和官方的dep,将会在后面的文章介绍,尽请期待。

订阅最新文章,欢迎关注我的公众号

微信公众号

参考

go依赖包管理工具对比