jinhailang / blog

技术博客:知其然,知其所以然
https://github.com/jinhailang/blog/issues
60 stars 6 forks source link

Go 依赖管理 #38

Open jinhailang opened 6 years ago

jinhailang commented 6 years ago

Go 依赖管理

Go 引用的包,可以分为两类:内部包和外部包,对于内部包,因为是项目内部自己写的,管理起来比较简单;但是,对于外部包,因为是开源的,一般由第三方维护管理,可能会版本不一致,出现非预期的情况,导致编译错误,或者出现程序 BUG,有经验的程序员应该都知道,这种由于第三方包引起的问题,定位和解决往往都是很烦人的。所以,目前所谓的依赖包管理工具,都是针对外部包的管理。

大部分编程语言都是将项目代码目录作为单独的工作目录(workspace),但 Go 只有一个工作目录 —— $GOPATH,因为 Go 自身提供了很多工具,固定的工作目录,便于这些工具的运行。因此,需要强调一点是,我们应该在 $GOPATH/src 下,创建自己的项目目录。因为,项目构建时,就是从这个路径下开始的(即作为环境路径)。 其实,对于 Go 本身来说,是不区分内部包和外部包的,因为 go get 下载的包,也都放在 $GOPATH/src 目录下,因此,当我们 import 依赖包时,只要直接路径指定就行了,类似 github.com/xx/x。又因为 $GOPATH 目录下可能创建了多个项目,因此下载在这里的依赖包,也可能会被多个项目引用,随时可能会被其他项目更新修改。

所以,在最开始的时候(Go 1.5 之前),go 项目包的依赖管理是很简陋的,为了保证外部包的安全,就必须要把外部包拷贝到项目内部,变成“内部包”。此时,包的查找顺序是: $GOROOT --> $GOPATH.

在 Go 1.5 时,为了解决这个问题,引入了 vendor 这一特殊目录,该目录在项目内创建,这就相当于,每个项目有了自己独立的 GOPATH,此时,包的查找顺序是:离引用代码最近的 vendor --> $GOROOT --> $GOPATH. 项目内的每个目录下都能创建自己的 vendor 目录,引用时,使用的是最近一层的 vendor。但是,最好只在根目录创建 vendor,便于管理,也避免同样的包,出现在同一项目内的多个 vendor 目录下。

基于 vendor 能够实现真正意义的依赖包管理,而告别简陋的拷贝操作。

内部包管理

内部包管理其实很简单,但是有一个特性需要说明,那就是 internal 目录,位于 internal 目录下的包,只能被同父目录下的包引用,否则会编译报错。 这里需要说明的是,父目录的父目录也算,即位于项目根目录下 internal 里面的包,是可以被项目内所有包引用的。 与 vendor 类似,项目内也可以有多个 internal 目录,但是建议在项目根目录创建一个就够了。

internal 目录的目的是保护该目录下的包被其他项目所引用,例如某个包还不太成熟,不想被其他项目使用。很明显,这只是一种简单的“警告”机制。

外部包管理

基于 vendor 实现的依赖包管理工具,比较流行的有 dep, Godep, Glide, Govendor 等,实现原理大同小异。官网这里对这几个工具进行了分类说明,建议使用 dep 作为项目的依赖管理工具,原因主要有两点:

dep 使用

dep 的使用,官方有较详细的说明文档:dep introduction.

这里根据自己的理解,做些说明。

安装

主要有两个安装方式:

   mv dep-linux-amd64 $GOPATH/bin/dep
   curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

当然你也可以使用源码安装,但是,你最好不要这么干。安装完成后,执行 dep -h 查看帮助信息。

使用

cd $GOPATH/src/dep-test
dep init

完成后,在项目根目录下会创建文件 Gopkg.lock, Gopkg.tomlvendor 目录,vendor 里面就是依赖的包源码了,前两个文件保存内保存了依赖包的规则类型信息,依赖包版本信息以及依赖关系描述,后面还会对 dep 实现原理进行简单说明。

实际应用中,提交代码时,我们可以将 vendor 目录一起提交,此时 其他开发者应该先使用 dep check 检验。 但是,一般 vendor 目录都比较大,这时可以只提交 Gopkg.tomlGopkg.lock 文件,开发者在本地使用 dep ensure -vendor-only 自行下载依赖包。

实现模型

实现模型也有详细说明:Models and Mechanismsdep 使用 four state system 模型,这是一种经典的包管理模型。这四个模块分别是:

dep 主要就是围绕对这四个模块进行输入输出管理实现。流程大概如下:

dep 根据项目引用代码和 Gopkg.toml 文件内的规则信息,计算自动生成 Gopkg.lock 文件,此时,依赖的包信息就确定了,然后根据这些信息将对应的包下载到 vendor 目录内。

踩过的坑

使用软链接问题

对于 go 这种必须将项目放在 $GOPATH/src 的约定,实际应用中可能并不灵活,于是有两个技巧:

但是,当使用软链接这种方式的时候,vendor 目录会失效,变成普通的目录了,因为 vendor 目录只有位于 $GOPATH 路径下,Go 才会把它当作特殊目录处理。而符号链接只是创建了指向文件名的符号,实际的文件位置依然没变。

使用相对路径问题

实际上对于内部包,我们在引用时可以使用相对路径(相对 import 代码的路径,例如: ./a/b 或者 ../x/y 等),这可能是 go 命令的潜规则,这种方式相对较灵活而且直观,项目存放位置也可以很随意。所以对于简单项目,个人是比较喜欢使用这种方式的。

但是,这会影响 dep 生成依赖关系,导致 dep 误以为没有依赖的外部包,非常奇怪的问题。

其实,这两个坑都是因为我之前比较随意,没有按规范,将项目放在 $GOPATH/src 导致的。

Go Modules

dep 还没有应用推广,在 Go 1.11 中,又推出了全新的依赖包管理机制 Go Modules,原来叫 vgo ,在 Go 1.11 被采纳合并到了主分支,正式被发布,不出意外的话,后续版本应该都会将这个当作包依赖管理的官方解决方案。

模块为 GOPATH 提供了替代方案,用来为项目定位依赖和管理版本化。如果 go 命令在 $GOPATH/src 之外的目录中运行,并且该目录中有一个模块的话,那么模块功能就会启用,否则 go 将会使用 GOPATH(Go 1.11)。与 dep 不同,模块是集成在 go 命令的,可以使用 go mod init 创建。

小结

Go 作为比较新的语言,包依赖管理方式也在持续变动,主要分为三个阶段: 纯依赖包源码拷贝 --> 基于 vendor --> Go Modules

每次变动都使管理变的更先进,这对于 Go 和 Gopher 都是有益的,特别是 Go Modules 据称使用了其它语言包管理工具不同的理念算法,看起来比较复杂。因为刚出来,有较多问题需要讨论,后续比较成熟了再研究,可能要等到 Go 1.12 版本吧。

所以,如果不想成为第一个吃螃蟹的人,目前,还是使用 dep 作为管理工具比较稳妥。