Go 依赖管理

Golang包管理工具glide简介

前言

Golang是一个十分有趣,简洁而有力的开发语言,用来开发并发/并行程序是一件很愉快的事情。在这里我感受到了其中一些好处:

  • 没有少了许多代码格式风格的争论,强制统一的风格多好;
  • 编译速度超快,再也不用等待许久,才能编译完工程(测试驱动开发自然更爽);
  • 也不会出现同一个项目组中的人,在使用同一个语言的不同子集。但这种情况不论是在C#还是在Java的世界里,都还是普遍存在的;
  • 轻松跨平台(当然Java做得不错,C#/.net还是努力中...)
  • 这是一门非常简洁、简单、清晰的编程语言(关键字好少啊)
  • 包依赖处理得很有趣;
  • 错误处理机制很有趣,个人觉得比C#/Java的异常处理机制更方便,也更合理些;
  • 对面向对象编程很有趣,非侵入性的接口实现方式,太赞了。
  • 用组合而不是继承
    有些东西没有,比如模板,这个东西有一大片争论,官方也有明确的回应。是啊,为什么一定要模板呢?非要不可吗?这么关键吗?如果非要不可,是不是go并不是这个环境下的最佳选择呢。

还有一个问题的是包管理,并没有官方最佳管理方案,在go的世界里存在大量的自制解决方案。go语言的包是没有中央库统一管理的,通过使用go get命令从远程代码库(github.com,goolge code 等)拉取,直接跳过中央版本库的约束,让代码的拉取直接基于源代码版本控制库,开发者间的协同直接依赖于源代码的版本控制。直接去除了库版本的概念。没有明显的包版本标识,感觉还是有点不适应,官方的建议是把外部依赖的代码全部复制到自己可控的源代码库中,进行同意管理。从而做到对依赖包的可控管理。

1.5版本的vendor目录特性后,官方wiki推荐了多种支持这种特性的包管理工具如:Godep、gv、gvt、glide、Govendor等。我比较喜欢glide。此外,作为程序员FQ似乎是必备技能,翻得一手好墙,才能跟得上步伐,才有收起刀落的手感,怎一个爽字了得。别问我怎么FQ,找度娘,她知道。

glide

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.yaml中依赖名称

$ glide name

查看依赖列表

$ glide list

查看帮助

$ glide help

参看glide版本信息

$ glide --version

无论何种语言,依赖管理都是一个比较复杂的问题。而Go语言中的依赖管理机制目前还是让人比较失望的。在1.6版本之前,官方只有把依赖放在GOPATH中,并没有多版本管理机制;1.6版本(1.5版本是experimental feature)引入vendor机制,是包依赖管理对一次重要尝试。他在Go生态系统中依然是一个热门的争论话题,还没有想到完美的解决方案。

看其它

我们先来看看其它语言怎么解决,例举两种典型的管理方式:

Java

开发态,可以通过maven和gradle工具编辑依赖清单列表/脚本,指定依赖库的位置/版本等信息,这些可以帮助你在合适的时间将项目固化到一个可随时随地重复编译发布的状态。这些工具对我来说已经足够优雅有效。但maven中也有不同依赖库的内部依赖版本冲突等令人心烦的问题。尤其是在大型项目中的依赖传递问题,若团队成员对maven机制没有足够了解下,依赖scope的滥用,会让整个项目工程的依赖树变得特别的巨大而每次编译效率低下。运行态,目前Java也没有很好的依赖管理机制,虽有classloader可以做一定的隔离,但像OSGi那种严格的版本管理,会让使用者陷入多版本相互冲突的泥潭。

Node.js

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 get

Go对包管理一定有自己的理解。对于包的获取,就是用go get命令从远程代码库(GitHub, Bitbucket, Google Code, Launchpad)拉取。这样做的好处是,直接跳过了包管理中央库的的约束,让代码的拉取直接基于版本控制库,大家的协作管理都是基于这个版本依赖库来互动。细体会下,发现这种设计的好处是去掉冗余,直接复用最基本的代码基础设施。Golang这么干很大程度上减轻了开发者对包管理的复杂概念的理解负担,设计的很巧妙。

当然,go get命令,仍然过于简单。对于现实过程中的开发者来说,仍然有其痛苦的地方:

  • 缺乏明确显示的版本。团队开发不同的项目容易导入不一样的版本,每次都是get最新的代码。尤其像我司对开源软件管理非常严格,开源申请几乎是无法实施。
  • 第三方包没有内容安全审计,获取最新的代码很容易引入代码新的Bug,后续运行时出了Bug需要解决,也无法版本跟踪管理。
  • 依赖的完整性无法校验,基于域名的package名称,域名变化或子路径变化,都会导致无法正常下载依赖。我们在使用过程,发现还是有不少间接依赖包的名称已失效了(不存在,或又fork成新的项目,旧的已不存维护更新)。

而Go官方对于此类问题的建议是把外部依赖的代码复制到你的 源码库中管理 。把第三方代码引入自己的代码库仍然是一种折中的办法,对于像我司的软件开发流程来说,是不现实的:

  • 开源扫描会扫描出是相似的代码时,若License不是宽松的,则涉及到法律风险,若是宽松的,开源扫描认证确认工作也很繁琐。
  • 如何升级版本,代码复制过来之后,源始的项目的代码可以变化很大了,无明显的版本校验,借助工具或脚本来升级也会带来工作量很大。
  • 复制的那一份代码已经开始变成私有,第三方代码的Bug只能自己解决,难以贡献代码来修复Bug,或通过推动社区来解决。
  • 普通的程序问题可能不是很大问题,最多就是编译时的依赖。但如果你写的是一个给其他人使用的lib库,引入这个库就会带来麻烦了。你这个库被多人引用,如何管理你这个库的代码依赖呢?

好在开源的力量就是大,Go官方没有想清楚的版本管理问题,社区就会有人来解决,我们已经可以找到许多不错的解决方案,不妨先参考下 官方建议 。

vendor机制

vendor是1.5引入为体验,1.6中正式发布的依赖管理特性。Go团队在推出vendor前已经在Golang-dev group上做了长时间的调研。最终Russ Cox在 Keith Rarick 的proposal的基础上做了改良,形成了Go 1.5中的vendor:

  • 不rewrite gopath
  • go tool来解决
  • go get兼容
  • 可reproduce building process

并给出了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是一个特殊的目录,在应用的源码目录下,go doc工具会忽略它。
  • vendor机制支持嵌套vendor,vendor中的第三方包中也可以包含vendor目录。
  • 若不同层次的vendor下存在相同的package,编译查找路径优先搜索当前pakcage下的vendor是否存在,若没有再向parent pacakge下的vendor搜索(x/y/z作为parentpath输入,搜索路径:x/y/z/vendor/path->x/y/vendor/path->x/vendor/path->vendor/path)
  • 在使用时不用理会vendor这个路径的存在,该怎么import包就怎么import,不要出现import “d/vendor/p”的情况。vendor是由go tool隐式处理的。
  • 不会校验vendor中package的import path是否与canonical import路径是否一致了。

vendor机制看似像node.js的node_modules,支持嵌套vendor,若一个工程中在着两个版本的相的包,可以放在不同的层次的vendor下:

  • 优点:可能解决不同的版本依赖冲突问题,不同的层次的vendor存放在不同的vendor。
  • 缺点:由于go的package是以路径组织的,在编译时,不同层次的vendor中相同的包会编译两次,链接两份,程序文件变大,运行期是执行不同的代码逻辑。会导致一些问题,如果在package init中全局初始化,可能重复初化出问题,也可能初化为不同的变量(内存中不同),无法共享获取。像之前我们遇到gprc类似的问题就是不同层次的相同package重复init导致的,见社区反馈 。

所以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 语言中,我们可以使用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目录下会包含pkgsrcbin三个子目录,这三个目录各有用处。

  • bin 目录用来放置编译好的可执行文件,为了使得这里的可执行文件可以方便的运行, 在 shell 中设置PATH变量。
  • src 目录用来放置代码源文件,在进行import时,是使用这个位置作为根目录的。自己编写的代码也应该放在这下面。
  • pkg 用来放置安装的包的链接对象(Object)的。这个概念有点类似于链接库,Go 会将编译出的可连接库放在这里, 方便编译时链接。不同的系统和处理器架构的对象会在pkg存放在不同的文件夹中。

我的GOPATH目录树如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── bin
├── pkg
│   └── darwin_amd64
│   └── github.com
│   └── zenazn
│     └── goji
└── src
├── code.google.com
│   └── p
│   └── go.crypto
└── github.com
   └── zenazn
   └── goji

一般来说,你自己的代码不应该直接放置在src目录下,而应该为其建立对应的项目文件夹。 go get也会把第三方包的源代码放到这个目录下,因此一般推荐设置两个GOPATH,比如:

1
export GOPATH="/usr/local/share/go:$HOME/codes/go"

这样第三方包就会默认放置在第一个路径中,而你可以在第二个路径下编写自己的代码。 虽然 Go 语言本身已经提供了相当强大的包管理方式了,但是仍然有一些不足:

  1. 不能很方便地隔离不同项目的环境
  2. 不能很方便地控制某个依赖包的版本
  3. 不能管理 Go 本身的版本

因此我们还需要一些第三方的工具来弥补这些缺陷。

第三方的管理工具

GOPATH 管理和包管理

由于存在GOPATH的机制,我们可以使用多个GOPATH来实现项目隔离的方法。 譬如,对于每个项目,都分配一个不同的路径作为GOPATH。 可以实现这样的目的的工具有gvp等。

对于 gvp 来说,想要针对当前目录建立一个GOPATH,只需要执行gvp init即可。 gvp 会在当前项目的目录下新建一个隐藏的文件夹作为GOPATH指向的位置。 切换环境时使用下面两个命令来修改环境变量。这种做法跟 Python 中的virtualenv比较类似。

1
2
source gvp in   # 进入当前目录对应的 GOPATH 环境
source gvp out # 登出当前目录对应的 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 语言,一般来说并没有使多个语言版本并存的需求。Go 语言现在还没有经历过类似 Python 2.x 到 3.x 或者 Ruby 1.x 到 2.x 这样破坏性的版本升级。旧的代码在新的语言版本当中一般是能够正确运行的。 不过若遇到非要并存多个版本的时候,gvm就是一个不错的选择。

gvm 的使用跟 rvm 比较类似。

1
2
gvm install go1 # 安装 go1 版本
gvm use go1 # 修改环境变量使用 go1 版本的 Go

总结

是否有必要使用多个 Workspace 仍然具有争议,譬如这个 StackOverflow 上的相关问答中, 就有人提出只使用一个 Workspace 就可以应付大多数情况了。

在研究相关问题的时候,我发现很多 Go 语言的用户都还带着原来编程语言的思维, 这点从上面介绍的多个工具的特点当中就可以很容易看出来:gvpgpm就是典型的 Python 的包管理模式, gvp对应着virtualenvgpm对应着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


你可能感兴趣的:(Golang,go)