Go基础-包管理

摘要

Go 语言的模块管理经历了三个重要的阶段

  • 第一阶段是通过全局的 GOPATH 来管理所有的第三方包
  • 第二阶段是通过 Vendor 机制将项目的依赖包局部化
  • 第三阶段是 Go 语言的最新功能 Go Module

内置的系统包

o 语言的内置包都是已经编译好的「包对象」,使用时编译器不需要进行二次编译。

1
2
3
4
5
6
$ go env
---
GOARCH="amd64"
GOOS="darwin"
GOROOT="/usr/local/Cellar/go/1.11.2/libexec"
---

Go 语言允许包路径带有网站域名,这样它就可以使用 go get 指令直接去相应的网站上拉去包代码。最常用的要数

  • github.com
  • gopkg.in
  • golang.org

Go 语言不存在官方维护的集中包仓库,它将包的选择分散到开源社区网站。使用量最大的要数 github.com,我们平时使用的大部分第三方包都是来源于此。也可以使用自己公司提供的代码仓库,路径名用上公司代码仓库的域名即可。默认会使用 https 协议下载代码仓库 ,可以使用 -insecure 参数切换到 http 协议。

模块的标准结构

  • 一个包的目录名可以与包名不一样
  • 当前目录下所有的文件都是同一个包名
  • 包内的代码互相引用使用全路径导入
  • 其他项目导入时用的全路径,使用的时候用的是包名
  • 支持相对路径导入,但是不推荐使用

两个包名字一样怎么处理

如果你的代码需要使用两个包,这两个包的路径最后一个单词是一样的,那该如何分清使用的是那个包呢?为了解决这个问题,Go 语言支持导入语句名称替换功能

1
2
import pmathy "github.com/pyloque/mathy"
import omathy "github.com/other/mathy"

无名导入

Go 语言还支持一种罕见的导入语法可以将其它包的所有类型变量都导入到当前的文件中,在使用相关类型变量时可以省去包名前缀。

1
2
3
4
5
6
7
8
9
package main

import "fmt"
import . "github.com/pyloque/mathy"

func main() {
fmt.Println(Fib(10))
fmt.Println(Fact(10))
}

但是这种用法很少见,而且非常不推荐使用

匿名导入

Go 语言还支持匿名导入,就是说你导入了某个第三方包,但是不需要显示使用它,这时就可以使用匿名导入。什么时候需要导入某个包而不使用呢?这是因为 Go 语言的代码文件中可以存在一个特殊的 init() 函数,它会在包文件第一次被导入的时候运行。

当我们使用数据库驱动的时候就会经常遇到匿名导入,第三方驱动包会在 init() 函数中将当前驱动注册到全局的驱动列表中,这样通过特定的 URI 就可以识别并找到相应的驱动来使用。

1
2
3
4
5
6
7
8
9
10
//github.com/go-sql-driver/mysql/driver.go
func init() {
sql.Register("mysql", &MySQLDriver{})
}

//使用
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)

当我们使用 Go 语言自带的图像处理包时也会遇到匿名导入,在对图像进行编码解码的时候需要根据不同的图像编码选择不同的逻辑。

1
2
3
4
5
6
import (  
"image"
_ "image/gif"
_ "image/png"
_ "image/jpeg"
)

包名和目录名不一样

Go 语言允许包名和当前的目录名成不一样,在导入包的时候使用的是目录路径,但是在使用的时候应该使用目录下的包名。所以你会看到导入的路径尾部和真正使用时的包名前缀不一样。

1
2
3
4
import "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary
json.Marshal(&data)

为什么 json-iterator 会使用这样奇怪的包路径呢,因为它要支持多种语言的,直接将最后的目录名改成语言的名称更加易于辨识。

Go常用指令

Go 提供了三个比较的常用的指令用来进行全局的包管理。

go build

仅编译。如果当前包里有 main 包,就会生成二进制文件。如果没有 main 包,build 指令仅仅用来检查编译是否可以通过,编译完成后会丢弃编译时生成的所有临时包对象。这些临时包包括自身的包对象以及所有第三方依赖包的包对象。如果指定 -i 参数,会将编译成功的第三方依赖包对象安装到 GOPATH 的 pkg 目录。

go install

先编译,再安装。将编译成的包对象安装到 GOPATH 的 pkg 目录中,将编译成的可执行文件安装到 GOPATH 的 bin 目录中。如果指定 -i 参数,还会安装编译成功的第三方依赖包对象。

go get

下载代码、编译和安装。安装内容包括包对象和可执行文件,但是不包括依赖包。

注意编译过程中第三方包的 main 包是不可能被编译的,安装的对象也就不可能包括第三方依赖包的可执行文件。

当我们使用 go run 指令来测试运行正在开发的程序时,如果发现启动了很久,这时候可以考虑先执行 go build -i 指令,将编译成功的依赖包都安装到 GOPATH 的 pkg 目录下,这样再次运行 go run 指令就会快很多。

1
2
$ go build -i
$ go run main.go

当我们使用的第三方包已经比较陈旧,可以使用 go get -u 指令拉取最新的依赖包。

1
$ go get -u github.com/go-redis/redis

局部管理 Vendor

当我们在本地要开发多个项目时,如果不同的项目需要依赖某个第三方包的不同版本,这时候仅仅通过全局的 GOPATH 来存放第三方包是无解的。解决方法有一个,那就是需要在不同的项目里设置不同的 GOPATH 变量来解决冲突问题。但是这还是不能解决一个重要的问题,那就是当我们的项目依赖了两个第三方包,这两个第三方包又同时依赖了另一个包的两个不同版本,这时候就会再次发生冲突。这种多版本依赖有一个专业的名称叫「钻石型」依赖。

为了解决这个问题,Go 1.6 引入了 vendor 机制。这个机制非常简单,就是在你自己项目的目录下增加一个名字为 vendor 子目录,将自己项目依赖的所有第三方包放到 vendor 目录里。这样当你导入第三方包的时候,优先去 vendor 目录里找你需要的第三方包,如果没有,再去 GOPATH 全局路径下找。

然后每个第三方项目都会有自己的 vendor 子目录,如此递归下去,可以想象,一个大型项目将会有一颗很深的依赖树。不过实际上这颗依赖数没你想象的那么深,因为 Go 的第三方开源包普遍比较轻量级,依赖不是很多。毕竟 Go 语言已经将很多互联网常用的工具包都内置了。

使用 vendor 有一个限制,那就是你不能将 vendor 里面依赖的类型暴露到外面去,vendor 里面的依赖包提供的功能仅限于当前项目使用,这就是 vendor 的「隔离沙箱」。正是因为这个沙箱才使得项目里可以存在因为依赖传递导致的同一个依赖包的多个版本。同时这也意味着项目里可能存在多份同一个依赖包,即使它们是同一个版本。比如你的包在 vendor 里引入了某个第三方包 A,然后别人的项目在 vendor 里引入你的包,同时它也引入第三方包 A。这就会导致生成的二进制文件变大,也会导致运行时内存变大,不过也无需担心,这点代价对于服务端程序来说基本可以忽略不计。

讲到这里还有一个很重要的问题没有解决,github 上有很多开源项目,这些项目都有多个版本号,我如何引入具体某一个版本呢?如果使用 go get 指令,它总是引入 master 分支的最新代码,它往往不是稳定的可靠代码。这就需要 Go 语言的依赖管理工具的支持了,它就好比 java 语言的 maven 工具,python 语言的 pip 工具。

Dep

Go 语言没有内置 vendor 包管理工具,它需要第三方工具的支持。这样的工具很多,目前最流行的要数 golang/dep 项目了,它差一点就被官方收纳为内置工具了,很可惜!上图是它的 Logo,图中叠起来的箱子就是 dep 正在管理的各种第三方依赖包。使用它之前我们需要将 dep 工具安装到 GOPATH 下面

1
$ go get github.com/golang/dep

同时需要将 ~/go/bin 目录加入到环境变量 PATH 中,因为 dep 可执行文件默认会安装到 ~/go/bin 中。但是令人意外的是 dep 居然表示不能直接解决「钻石型」依赖,这让我感受到了它的危机,在 dep 中依赖包是扁平化的,vendor 不允许嵌套。如果出现了版本冲突,需要使用某种特殊手段来解决。

配置文件

dep 管理的项目会有两个配置文件,分别是 Godep.toml 和 Godep.lock。Godep.toml 用于配置具体的依赖规则,里面包含项目的具体版本号信息。通过 toml 配置文件,你即可以使用远程的依赖包(github),也可以直接使用本地的依赖包(GOPATH)。还可以为依赖包指定别名,这样就可以在代码里使用和真实路径不一样的导入路径。当你需要切换依赖包的不同版本时,可以在 toml 配置文件里修改依赖的版本号,然后通过 dep ensure 指令来更新依赖项。

Gopkg.lock 是基于当前的 toml 文件配置规则和项目代码来生成依赖的精确版本,它确定了 vendor 文件夹里要下载的依赖项代码的目标版本。

dep init

该指令用于初始化当前的项目,它会静态分析当前的项目代码(如有有的话),生成 Godep.toml 和 Godep.lock 依赖配置文件,将依赖的项目代码下载到当前项目的 vendor 文件夹里面。它会根据一定的策略来选择最新的依赖包版本。如果自动策略生成的版本号不是你想要的,可以再修改配置文件执行 dep ensure 来切换其它版本。

dep ensure

该指令会下载代码里用到的新依赖项、移除当前项目代码里不使用的依赖项。确保当前的依赖包代码和当前的项目代码配置处于完全一致的状态。

dep ensure -update

更新 Godep.lock 文件中的所有依赖项到最新版本。可以增加 一到多个包名参数,指定更新特定的依赖包。如果 toml 配置文件限定了依赖包的版本范围,那么更新必须遵守 toml 规则的版本限制。

dep ensure -add github.com/a/b

增加并下载一个新的项目依赖包,可以指定依赖版本号。如 dep ensure -add github.com/a/b@master 或者 github.com/a/b@1.0.0

dep status

显示当前项目的依赖状态。

Dep 在使用起来比较简单,但是其内部实现上是一个比较复杂的工具,鉴于篇幅限制,本节就不再继续深入讲解 Dep 了,以后有空再单独开启一篇来深入探讨吧。我甚至觉得理解 Dep 已经变得没有那么必要,因为它已经被 Go 语言官方抛弃了,取而代之的解决方案是 Go Module。