Golang是一个十分有趣,简洁而有力的开发语言,用来开发并发/并行程序是一件很愉快的事情。在这里我感受到了其中一些好处:
还有一个问题的是包管理,并没有官方最佳管理方案,在go的世界里存在大量的自制解决方案。go语言的包是没有中央库统一管理的,通过使用go get命令从远程代码库(github.com,goolge code 等)拉取,直接跳过中央版本库的约束,让代码的拉取直接基于源代码版本控制库,开发者间的协同直接依赖于源代码的版本控制。直接去除了库版本的概念。没有明显的包版本标识,感觉还是有点不适应,官方的建议是把外部依赖的代码全部复制到自己可控的源代码库中,进行同意管理。从而做到对依赖包的可控管理。
1.5版本的vendor目录特性后,官方wiki推荐了多种支持这种特性的包管理工具如:Godep、gv、gvt、glide、Govendor等。我比较喜欢glide。此外,作为程序员FQ似乎是必备技能,翻得一手好墙,才能跟得上步伐,才有收起刀落的手感,怎一个爽字了得。别问我怎么FQ,找度娘,她知道。
glide是Go的包管理工具。支持语义化版本,支持Git、Svn等,支持Go工具链,支持vendor目录,支持从Godep、GB、GPM、Gom倒入,支持私有的Repos和Forks。
使用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.
$ curl https://glide.sh/get | sh
$ glide init
初始化,glide扫描代码目录,创建一个glide.yaml文件,文件中记录了所有的依赖
$ edit glide.yaml
通过修改glide.yaml文件,可以添加版本信息等,这一步不是必须的。
$ glide update
或者
$ glide up
下载和更新glide.yaml中列出的所有依赖包,并将它们放到vendor目录下。glide同时也递归获取依赖包需要的任何依赖项包括配置文件中定义的依赖项目。glide递归获取依赖,可以识别Glide、Godep、gb、gom和GPM管理的项目。
当依赖被制定到特定的版本时,名为glide.lock的文件会被创建或者更新。例如,如果在glide.yaml中一个版本被指定在一个范围内(如:^1.2.3),那么glide将在glide.yaml中设定一个特定提交ID(commit id)。如此,将允许重复安装(见 glide install命令)。
从获取的依赖包中移除嵌套的vendor/目录可以使用-v标记。
当需要从glide.lock文件中安装制定版本的包是,可以使用install命令:
glide install
该命令将会读取glide.lock文件,当glide.lock文件和glide.yaml不同步时,如glide.yaml发生改变,glide将会提供一个警告。运行glide up命令更新依赖树时,将会重建glide.lock文件。
$ glide name
$ glide list
$ glide help
$ glide --version
无论何种语言,依赖管理都是一个比较复杂的问题。而Go语言中的依赖管理机制目前还是让人比较失望的。在1.6版本之前,官方只有把依赖放在GOPATH中,并没有多版本管理机制;1.6版本(1.5版本是experimental feature)引入vendor机制,是包依赖管理对一次重要尝试。他在Go生态系统中依然是一个热门的争论话题,还没有想到完美的解决方案。
我们先来看看其它语言怎么解决,例举两种典型的管理方式:
开发态,可以通过maven和gradle工具编辑依赖清单列表/脚本,指定依赖库的位置/版本等信息,这些可以帮助你在合适的时间将项目固化到一个可随时随地重复编译发布的状态。这些工具对我来说已经足够优雅有效。但maven中也有不同依赖库的内部依赖版本冲突等令人心烦的问题。尤其是在大型项目中的依赖传递问题,若团队成员对maven机制没有足够了解下,依赖scope的滥用,会让整个项目工程的依赖树变得特别的巨大而每次编译效率低下。运行态,目前Java也没有很好的依赖管理机制,虽有classloader可以做一定的隔离,但像OSGi那种严格的版本管理,会让使用者陷入多版本相互冲突的泥潭。
npm是Node.js的首选模块依赖管理工具。npm通过一个当前目录的 package.json 文件来描述模块的依赖,在这个文件里你可以定义你的应用名称( name )、应用描述( description )、关键字( keywords )、版本号( version )等。npm会下载当前项目依赖模块到你项目中的一个叫做node_modules的文件夹内。与maven/gradle不同的是,maven最终会分析依赖树,把相同的软件默认扁平化取最高版本。而npm支持nested dependency tree。nested dependency tree是每个模块依赖自己目录下node_modules中的模块,这样能避免了依赖冲突, 但耗费了更多的空间和时间。由于Javascript是源码发布,所以开发态与运行态的依赖都是基于npm,优先从自己的node_modules搜索依赖的模块。
Go对包管理一定有自己的理解。对于包的获取,就是用go get命令从远程代码库(GitHub, Bitbucket, Google Code, Launchpad)拉取。这样做的好处是,直接跳过了包管理中央库的的约束,让代码的拉取直接基于版本控制库,大家的协作管理都是基于这个版本依赖库来互动。细体会下,发现这种设计的好处是去掉冗余,直接复用最基本的代码基础设施。Golang这么干很大程度上减轻了开发者对包管理的复杂概念的理解负担,设计的很巧妙。
当然,go get命令,仍然过于简单。对于现实过程中的开发者来说,仍然有其痛苦的地方:
而Go官方对于此类问题的建议是把外部依赖的代码复制到你的 源码库中管理 。把第三方代码引入自己的代码库仍然是一种折中的办法,对于像我司的软件开发流程来说,是不现实的:
好在开源的力量就是大,Go官方没有想清楚的版本管理问题,社区就会有人来解决,我们已经可以找到许多不错的解决方案,不妨先参考下 官方建议 。
vendor是1.5引入为体验,1.6中正式发布的依赖管理特性。Go团队在推出vendor前已经在Golang-dev group上做了长时间的调研。最终Russ Cox在 Keith Rarick 的proposal的基础上做了改良,形成了Go 1.5中的vendor:
并给出了vendor机制的”4行”诠释:
If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import “p” is interpreted as import “d/vendor/p” if that exists.
When there are multiple possible resolutions,the most specific (longest) path wins.
The short form must always be used: no import path can contain “/vendor/” explicitly.
Import comments are ignored in vendored packages.
总结解释起来:
vendor机制看似像node.js的node_modules,支持嵌套vendor,若一个工程中在着两个版本的相的包,可以放在不同的层次的vendor下:
所以Russ Cox期望大家良好设计工程布局,作为lib的包 不携带vendor更佳 ,一个project内的所有vendor都集中在顶层vendor里面。
Go的包依赖问题依旧困扰着开发人员,嵌套vendor可以一定程度解决多版本的依赖冲突问题,但也引入多份编译导致的问题。目前社区也在一直讨论如何更好的解决,将进入下一个改进周期。这次将在Peter Bourgon的主持下正式启动: go packaging proposal process ,当前1.8版本特性已冻结,不知这个改进是否会引入到1.9版本中。
参考:
[1] 理解Go 1.5 vendor
[2] Golang的包管理之道
在 Go 语言中,我们可以使用go get
命令安装远程仓库中托管的代码,不同于 Ruby Gem、pypi 等集中式的包管理机制, Go 语言的包管理系统是去中心化的。简单来讲,go get
命令支持任何一个位置托管的 Git 或 Mercurial 的仓库,无论是 Github 还是 Google Code 上的包,都可以通过这个命令安装。
我们知道,在 Go 语言中的import
语句对于已经使用go get
安装到本地的包,依然要使用其去绝对路径引入。 比如对于从 Github 上安装的 goji,其在 Github 上的路径 URL 是https://github.com/zenazn/goji
,因此在import
它的时候需要使用下面的代码:
1 |
import "github.com/zenazn/goji" |
正因为如此,Go 语言可以通过直接分析代码中的import
语句来查询依赖关系。 go get
命令在执行时,就会自动解析import
来安装所有的依赖。
除了go get
,Go 语言还提供了一个 Workspace 的机制,这个机制也是很容易让人困惑的设计。简单来说就是通过设定 GOPATH
环境变量,指定除了GOROOT
所指定的目录之外,Go 代码所在的位置(也就是 Workspace 的位置)。 一般来说,GOPATH
目录下会包含pkg
、src
和bin
三个子目录,这三个目录各有用处。
bin
目录用来放置编译好的可执行文件,为了使得这里的可执行文件可以方便的运行, 在 shell 中设置PATH
变量。src
目录用来放置代码源文件,在进行import
时,是使用这个位置作为根目录的。自己编写的代码也应该放在这下面。pkg
用来放置安装的包的链接对象(Object)的。这个概念有点类似于链接库,Go 会将编译出的可连接库放在这里, 方便编译时链接。不同的系统和处理器架构的对象会在pkg
存放在不同的文件夹中。 我的GOPATH
目录树如下所示:
1 |
├── bin |
一般来说,你自己的代码不应该直接放置在src
目录下,而应该为其建立对应的项目文件夹。 go get
也会把第三方包的源代码放到这个目录下,因此一般推荐设置两个GOPATH
,比如:
1 |
export GOPATH="/usr/local/share/go:$HOME/codes/go" |
这样第三方包就会默认放置在第一个路径中,而你可以在第二个路径下编写自己的代码。 虽然 Go 语言本身已经提供了相当强大的包管理方式了,但是仍然有一些不足:
因此我们还需要一些第三方的工具来弥补这些缺陷。
由于存在GOPATH
的机制,我们可以使用多个GOPATH
来实现项目隔离的方法。 譬如,对于每个项目,都分配一个不同的路径作为GOPATH
。 可以实现这样的目的的工具有gvp等。
对于 gvp 来说,想要针对当前目录建立一个GOPATH
,只需要执行gvp init
即可。 gvp 会在当前项目的目录下新建一个隐藏的文件夹作为GOPATH
指向的位置。 切换环境时使用下面两个命令来修改环境变量。这种做法跟 Python 中的virtualenv比较类似。
1 |
source gvp in # 进入当前目录对应的 GOPATH 环境 |
至于对依赖包更版本更细致的管理,可以配合的工具还有 gpm。 gpm
有点类似于 Python 中的pip工具。他可以生成一个名为 Godeps
的文件, 其中记录了每个依赖包的 URL 以及使用的版本(hash tag)。 之前的一篇文章提到 gpm
只能管理来自 Github 的依赖,不过当前的版本已经支持了非 Git 方式托管的依赖包了。
基于同样原理管理依赖包版本的工具还有Godep。 这个工具在 Github 上具有相当高的关注度。它所生成的Godeps
文件采用 JSON 格式储存, 是一个跟 Node.js 中 NPM 相仿的工具。
总体来说以上几个工具已经可以解决隔离项目环境和控制依赖包版本的问题了。但是使用上还不算方便, 为了能在我们 cd 到某个目录时自动的切换环境变量,我们可能还需要在 shell 做一些配置使其在cd
到项目目录下时自动切换环境变量。
这方面做的比较好的一个选择是 Go Manager(gom), 它生成的Gomfile
格式上几乎跟 Ruby Gem 一样。gom 可能是这些工具当中使用最方便的一个, 只要使用gom build
命令代替原来的go build
命令进行编译,你基本不需要配置 Shell 或者和环境变量打交道。
对于 Go 语言,一般来说并没有使多个语言版本并存的需求。Go 语言现在还没有经历过类似 Python 2.x 到 3.x 或者 Ruby 1.x 到 2.x 这样破坏性的版本升级。旧的代码在新的语言版本当中一般是能够正确运行的。 不过若遇到非要并存多个版本的时候,gvm就是一个不错的选择。
gvm 的使用跟 rvm 比较类似。
1 |
gvm install go1 # 安装 go1 版本 |
是否有必要使用多个 Workspace 仍然具有争议,譬如这个 StackOverflow 上的相关问答中, 就有人提出只使用一个 Workspace 就可以应付大多数情况了。
在研究相关问题的时候,我发现很多 Go 语言的用户都还带着原来编程语言的思维, 这点从上面介绍的多个工具的特点当中就可以很容易看出来:gvp
和gpm
就是典型的 Python 的包管理模式, gvp
对应着virtualenv
,gpm
对应着pip
;如果你之前是 Node.js 和 NPM 的用户, 那么GoDeps
肯定会让你有种熟悉的感觉;更不用说最后介绍的gom
了,它从名称到文件格式都在模仿 Ruby Gem。
不同编程背景的开发者来到 Go 语言之后各自带来了自己的依赖包管理方式,而且形成了各自的社区。 这种现象虽然使得各自圈子的开发者免去了选择恐惧症,但是造成的解决方案分裂和互不兼容的情况也需要正视。 这时我们不禁要问,Go 自己的解决方式应该是什么样的?Go 语言为何没有一个官方标准的解决方案呢?
从Go FAQ的一段文字当中我们可以得到部分答案:
Versioning is a source of significant complexity, especially in large code bases, and we are unaware of any approach that works well at scale in a large enough variety of situations to be appropriate to force on all Go users. (依赖包的版本管理是一个非常复杂的问题,特别是在代码量比较大的时候。 我们一直没有找到任何一种方式能够在各种情形下都能良好工作, 因此也没有一种方式足够好到应该强迫所有的 Go 用户使用它)
因此现阶段来看,对于 Go 语言的包管理解决方案,我们也就只能“仁者见仁,智者见智”了。
最后,对于想要了解 Go 语言的包管理以及更多可用的工具的读者,这里再推荐两篇相关的文章: Go Package Management 和 A Journey in Golang Package Manager