likes
comments
collection
share

一文搞定golang包管理工具

作者站长头像
站长
· 阅读数 7

Go 语言中的依赖管理经历了长时间的演进过程。在 Go1.5 之前,Go 官方通过 GOPATH 对依赖进行管理。但是由于 GOPATH 存在诸多问题,社区和官方尝试了诸多新的依赖管理工具,中间出现的 Godep、Glide、Vendor 等工具都不如人意,最终笑到最后的是在 Go 1.11 后引入,并在 Go 1.13 之后成为 Go 依赖管理主流的 Go Modules。

让我们先来看看 GOPATH 和它的不足之处。

GOPATH

什么是 GOPATH?

在 Go 1.8 及以上版本中,如果用户不指定 GOPATH,GOPATH 的路径就是默认的。我们可以通过输入 go env 或者 go env gopath 查看 GOPATH 的具体配置:

C:\\Windows\\system32> go env

set GOPATH=C:\\Users\\xxx\\go

...

GOPATH 的路径在 Mac 和 Linux 操作系统下为 $HOME/go,而在 Windows 操作系统下为 %USERPROFILE%\go。我们可以把 GOPATH 可以理解为 Go 语言的工作空间,它内部存储了 src、bin、pkg 三个文件夹:

go/

├── bin

├── pkg

└── src

GOPATH/bin目录

存储了通过goinstall安装的二进制文件。操作系统使用PATH 环境变量来查找不需要完整路径即可执行的二进制应用程序。建议将 GOPATH/bin目录添加到全局GOPATH/bin 目录添加到全局 GOPATH/bin目录添加到全局PATH 变量中。

$GOPATH/pkg目录

会有一个文件夹(文件名根据操作系统的不同而有所不同,例如在 Mac 操作系统下为 darwin_amd64)存储预编译的 obj 文件,以加快程序的后续编译。大多数开发人员不需要访问此目录。我们在后面还会看到,pkg 下的 mod 文件会存储 Go Modules 的依赖。

$GOPATH/src目录

存储项目的 Go 代码。通常包含多个由 Git 管理的存储库,每个存储库中都包含一个或多个 package,每个 package 有多个目录,每个目录下都包含一个或多个 Go 源文件。整个路径看起来是下面的样子:

go/

├── bin

└── main.exe

├── pkg

├── darwin_amd64

└── mod

└── src

├── github.com

│ ├── tylfin

│ │ ├── dynatomic

│ │ └── geospy

│ └── uudashr

│ └── gopkgs

└── golang.org

└── x

└── tools

在 Go 的早期版本中,可以使用 go get 指令从 GitHub 或其他地方获取 Go 项目代码。这时程序会默认将代码存储到 $GOPATH/src 目录下。例如,在拉取 go get github.com/dreamerjackson/theWayToGolang 时,目录结构如下所示:

go/

├── bin

├── pkg

└── src

└── github.com

└── dreamerjackson

└── theWayToGolang

当我们使用 go get -u xxx 时,会将该项目以及项目所依赖的所有其他项目一并下载到 $GOPATH/src 目录下。 在 GOPATH 模式下,如果我们在项目中导入了一个第三方包,例如import "github.com/gobuffalo/buffalo"。 那么,实际引用的是 $GOPATH/src/github.com/gobuffalo/buffalo 文件中的代码。

GOPATH 的落幕与依赖管理的历史

GOPATH 借鉴了谷歌内部使用的 Blaze 系统。在 Blaze 中,所有项目的源代码共用一个代码仓库。go get 仅仅需要获取 Master 分支上最新的代码,不需要指定依赖的版本。

GOPATH 这种版本管理方式配置简单,容易理解,在谷歌内部不会出现问题。但是在开源领域,一个 GOPATH 走天下的情况就行不通了。由于依赖的第三方包总是在变,而且还没有严格的约束,直接拉取外部包的最新版本时,甚至可能出现一更新依赖代码都编译不过的情况。因此,我们迫切需要新的依赖管理工具。

2015 年,Go1.5 版本首次实验性地加入了 Vendor 机制。项目的所有第三方依赖都可以存放在当前项目的 Vendor 目录下,再也不用为了应用不同版本的依赖对 GOPATH 环境变量“偷梁换柱”了,Go 编译器优先感知和使用 Vendor 目录下缓存的第三方包。

但即便有了 Vendor 的支持,Vendor 内第三方依赖包代码的管理依旧不规范,要么需要手动处理,要么是借助 Godep 这样的第三方包管理工具。在这期间,社区也出现了大量的依赖管理工具,有点乱花渐欲迷人眼的态势。直到 Go Modules 出现,一锤定音。

2018 年 8 月,Go 1.11 发布,Modules 作为官方试验一同发布。

2019 年 9 月,Go1.13 发布,只要目录下有 go.mod 文件,Go 编译器都会默认使用 Modules 来管理依赖。同时,新版本还添加了 GOPROXY、GOSUMDB、GOPRIVATE 等多个与依赖管理有关的环境变量。

GoVendor

Vendor特性

为了解决GOPATH方式 三方依赖与项目都放在GOPATH/src下的问题,Go1.5之后,官方推出了Go Vendor的方式来管理项目。

go vendor就是将依赖的包,特指外部包,复制到当前项目下的vendor目录下,这样go build的时候,go会优先从vendor目录寻找依赖包。

Go Vender的出现解决了GOPATH不能多版本控制的问题。

Go Vender放弃了依赖重用,使冗余度上升。同一个依赖包如果不同工程想复用,都必须各自复制一份在自己的vendor目录下。

GoVendor工具

开启方式

  • golang 1.5默认是关闭的,需要设置go env -w GO15VENDOREXPERIMENT=1 来手动开启。
  • golang 1.6默认开启
  • goalng 1.7 已去掉该环境变量,默认开启 vendor 特性。

那有了Vendor 特性,我们如何去用呢,总不能手动去搞吧? 于是govendor就出现了。

govendor 是一个基于 vendor 机制实现的 Go 包依赖管理命令行工具。

# 1.下载安装
go get -u github.com/kardianos/govendor
# 2.初始化项目
govendor init

项目根目录下即会自动生成 vendor 目录和 vendor.json 文件。此时 vendor.json 文件内容为:

{
    "comment": "",
    "ignore": "test",
    "package": [],
    "rootPath": "govendor-example"
}

常用命令

  • govendor add +external 将已被引用且在 $GOPATH 下的所有包复制到 vendor 目录
  • govendor add gopkg.in/yaml.v2 将指定包复制到 vendor 目录
  • govendor list 列出代码中所有被引用到的包及其状态
  • govendor list -v fmt 列出一个包被哪些包引用
  • govendor fetch golang.org/x/net/context 从远程仓库添加或更新某个包(不会在 $GOPATH 也存一份)
  • govendor fetch golang.org/x/net/context@v1 安装指定版本的包

还有很多,因为现在不怎么用了,不一一列举了,有需要可以去官网查看。

未解决的问题

无法精确的引用 外部包进行版本控制,不能指定引用某个特定版本的外部包,只是在开发时将其拷贝过来,但是一旦外部包升级,vendor 下面的包会跟着升级,而且 vendor 下面没有完整的引用包的版本信息, 对包升级带来了无法评估的风险。

GoModules

GoModule是Go 1.11版本中添加的一个功能,用于帮助管理Go项目中的依赖关系。 GoModules未出现之前,Go依赖于GOPATH环境变量来管理依赖关系。在这种模型中,所有Go包及其依赖项都存储在一个名为GOPATH的单个目录中。这种方法简单,但会导致一些问题,包括版本冲突、需要手动管理依赖关系以及在共享代码方面存在困难。为了解决这些问题并提供更好的依赖关系管理方式,引入了GoModule。

特点

在好的依赖管理出现之前,有一些问题长期困扰 Go 语言的开发人员:

  • 能不能让 Go 工程代码脱离 GOPATH?
  • 能否处理自动选择开发时使用的依赖代码?
  • 能否自动选择最兼容的依赖版本?
  • 能否在本地管理依赖项,自定义依赖项?

Go Modules 巧妙解决了上面这些问题。

Go工程代码脱离 GOPATH

在 GOPATH 中,“导入路径”与“项目在文件系统中的目录结构和名称”必须是匹配的。但是,如果我们希望项目的实际路径和导入路径不同, 例如 import 路径为 github.com/gobuffalo/buffalo,我们希望项目的实际路径在另一个任意的文件目录下 (例如,/users/gobuffalo/buffalo),这个期待能否实现呢?

答案是肯定的。Go Modules 可以通过在 go.mod 文件中指定模块名来解决这一问题。go.mod 文件如下所示:

## go.mod

01 module github.com/gobuffalo/buffalo

02

...

06

go.mod 文件的第一行指定了模块名,开发人员可以用模块名引用当前项目中任何 package 的路径名。这样,无论当前项目路径在什么位置,都可以使用模块名来解析代码内部的 import。

自动选择开发时使用的依赖代码

对于任何版本控制(VCS,version control system)工具,我们都能在任何提交的 commit 处打上 tag 标记。 开发人员可以使用 VCS 工具引用特定标签,将软件包的任何指定版本克隆到本地。当我们引用一个第三方包时,出于测试等不同的目的,可能并不总是希望应用项目最新的代码,而是想要应用某一个特定的,与当前项目兼容的代码。

对于特定第三方库来说,维护者可能并没有意识到有人在使用他们的代码,或者代码库由于某种原因进行了巨大的不兼容更新。因此,我们希望能够明确使用的第三方包的版本,这样才能完成可重复的构建,并且希望能够自动下载、管理依赖包。一个依赖管理工具至少需要考虑下面几个问题。

  • 如何查找并把所有的依赖包下载下来?
  • 某一个包下载失败怎么办?
  • 所有项目之间如何进行依赖的传导?
  • 如何选择一个最兼容的包?
  • 如何解决包的冲突?
  • 如何在项目中同时引用第三方包的两个不同版本?

因此,只通过 GOPATH 维护单一的 Master 包是远远不够的,Go 官方的 Go Modules 提供了一种可以在文件中同时维护直接和间接依赖项的集成解决方案。一个特定版本的依赖项也被叫做一个模块(moudle),一个模块是一系列指定版本的 package 的集合。

为了加快构建程序的速度,快速切换、获取项目中依赖项的更新,Go 维护了下载到本地计算机上的所有模块的缓存,缓存目前默认位于 $GOPATH/pkg 目录下:

go/

├── bin

├── pkg

├── darwin_amd64

└── mod

└── src

在 mod 目录下,我们能够看到模块名路径中的第一部分即为顶级文件夹,如下所示:

~/go/pkg/mod » ls -l

drwxr-xr-x 6 jackson staff 192 1 15 20:50 cache

drwxr-xr-x 7 jackson staff 224 2 20 17:50 cloud.google.com

drwxr-xr-x 3 jackson staff 96 2 18 12:03 git.apache.org

drwxr-xr-x 327 jackson staff 10464 2 28 00:02 github.com

drwxr-xr-x 8 jackson staff 256 2 20 17:27 gitlab.followme.com

drwxr-xr-x 6 jackson staff 192 2 19 22:05 go.etcd.io

...

当我们打开一个实际的模块时(例如 github.com/nats-io),会看到许多与 NATS 库有关的模块及其版本:

~/go/pkg/mod » ls -l github.com/nats-io

total 0

dr-x------ 24 jackson staff 768 1 17 10:27 gnatsd@v1.4.1

dr-x------ 15 jackson staff 480 2 17 22:22 go-nats-streaming@v0.4.0

dr-x------ 26 jackson staff 832 2 19 22:05 go-nats@v1.7.0

dr-x------ 26 jackson staff 832 1 17 10:27 go-nats@v1.7.2

为了拥有一个干净的工作环境,我们可以用下面的指令清空缓存区。但是要注意,在正常的工作流程中,是不需要执行这段代码的:

go clean -modcache

自动选择最兼容的依赖版本

Go Modules 最小版本选择原理

明白了 Go Modules 的使用方法,接下来我们来看一看 Go Modules 在复杂情况下的版本选择原理。

每个依赖管理解决方案都必须解决选择哪个依赖版本的问题。当前许多版本选择算法都倾向于选择依赖的最新版本。如果人们能够正确应用语义版本控制并且遵守约定,那么这是有道理的。在这些情况下,依赖项的最新版本应该是最稳定和最安全的,而且,它还应该和较早版本有很好的兼容性。

但是,Go 语言采用了其他方法,Go Team 技术负责人 Russ Cox 花费了大量时间和精力撰写和谈论 Go 的版本选择算法,即最小版本选择(Minimal Version Selection,MVS)。Go 团队相信 MVS 可以更好地为 Go 程序提供兼容性和可重复性。

那么什么是最小版本选择原理呢?

Go 最小版本选择指的是,在选择依赖的版本时,优先选择项目中最合适的最低版本。当然,并不是说 MVS 不能选择最新的版本,而是说如果项目中任何依赖都用不到最新的版本,那么我们本质上不需要它。

本地管理依赖项,自定义依赖项

下载依赖
go mod download

会根据go.mod自动下载依赖到$GOPATH/pkg/mod目录下,但不会更新go.mod文件。 也可以下载 go.mod 中单个依赖 go mod download github.com/jinzhu/now 但不能下载 go.mod 中没有的依赖,否则会报错。

获取依赖

为了能够将项目依赖的 package 下载到本地,我们可以使用 go mod tidy 指令。

go mod tidy 
go:finding github.com/dreamerjackson/mydiv latest 
go:downloading github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 
go:extracting github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161`

执行完毕后,在 go.mod 文件中增加了一行,指定了引用的依赖库和版本。

module github.com/dreamerjackson/mathlib 
go 1.13 

require github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161

注意 这里间接的依赖(即 github.com/dreamerjackson/mydiv 依赖的 github.com/pkg/errors)没有在 go.mod 文件中展示出来,而是在一个自动生成的新文件 go.sum 中进行了指定。

更新依赖

假设我们依赖的第三方包出现了更新怎么办?如何将依赖代码更新到最新的版本呢? 有多种方式可以实现依赖模块的更新,我们需要在 go.mod 文件中修改版本号为:

require github.com/dreamerjackson/mydiv latest

或者 :

require github.com/dreamerjackson/mydiv master

或者将指定 commit Id 复制到末尾:

require github.com/dreamerjackson/mydiv c9a7ffa8112626ba6c85619d7fd98122dd49f850

还有一种办法是在终端的当前项目中,运行 go get github.com/dreamerjackson/mydiv

使用上面任一方式保存文件后,再次运行 go mod tidy,版本即会进行更新。这个时候如果我们再打开 go.sum 文件会发现,go.sum 中不仅存储了直接和间接的依赖,还存储了过去的版本信息。

github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161 h1:QR1fJ05yjzJ0qv1gcUS+gAe5Q3UU5Y0le6TIb2pcJpQ= 
github.com/dreamerjackson/mydiv v0.0.0-20200305082807-fdd187670161/go.mod h1:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ= 
github.com/dreamerjackson/mydiv v0.0.0-20200305090126-c9a7ffa81126/go.mod h1:h70Xf3RkhKSNbUF8W3htLNJskYJSITf6AdEGK22QksQ= 
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
移除依赖

当我们不想使用此第三方包时,可以直接在代码中删除无用的代码,接着执行go mod tidy, 会发现 go.mod 和 go.sum 又空空如也了。

依赖复制到项目目录

将依赖转移至本地的vendor文件

go mod vendor

注意

  • 此命令产生的vendor
replace替换指定依赖

Go Modules 中还提供了其他的功能,除了require,还包括了replace、 exclude、 retract 等指令。replace 指令可以将依赖的模块替换为另一个模块,例如由公共库替换为内部私有仓库,如下所示。replace 还可以用于本地调试场景,这时我们可以将依赖的第三方库替换为本地代码,便于进行本地调试。

replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5 
replace ( 
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5 
    golang.org/x/net => example.com/fork/net v1.4.5 
    golang.org/x/net v1.2.3 => ./fork/net golang.org/x/net => ./fork/net 
    )
exclude排除特定依赖

有时我们希望排除某一模块特定的版本,这时就需要用到 exclude 指令了。如果当前项目中,exclude指令与require指令对应的版本相同,那么 go get 或 go mod tidy 指令将查找高一级的版本。

exclude golang.org/x/net v1.2.3 
exclude ( 
    golang.org/x/crypto v1.4.5 golang.org/x/text v1.6.7 
    )
retract撤回指令

retract撤回指令表示不依赖指定模块的版本或版本范围。当版本发布得太早,或者版本发布之后发现严重问题时,撤回指令就很有用了。例如,对于模块example.com/m,假设我们错误地发布了 v1.0.0 版本后想要撤销。这时,我们就需要发布一个新的版本,tag 为v1.0.1 。

retract ( v1.0.0, v1.0.1 )

然后,我们要执行 go get example.com/m@latest,这样,依赖管理工具读到最新的版本 v1.0.1 是撤回指令,而且发现 v1.0.0 和 v1.0.1 都被撤回了,go 命令就会降级到下一个最合适的版本,比如 v0.9.5 之类的。除此之外,retract  指令还可以指定范围,更灵活地撤回版本。

retract v1.0.0 retract [v1.0.0, v1.9.9] retract ( v1.0.0 [v1.0.0, v1.9.9] )

retract 特性效果 成功发布最新版本 v0.3.0 版本并指定 retract 后。 所有引用了该库的工程应用,执行 go list 就可以看到如下提醒:

go1.16 list -m -u all

github.com/eddycjy/awesomeProject
github.com/eddycjy/go-retract-demo v0.2.0 (retracted) [v0.3.0]

结合该命令,我们日常使用的 IDE(例如:GoLand),其在保存时是会默认执行 go list 命令的。在后续 IDE 支持后,就可以在编码时就快速发现有问题的版本和提示

GO111MODULE

GO111MODULE是Go编程语言中的一个设置,用于控制GoModule是否开启使用。

GO111MODULE是一个环境变量,用于控制GoModule是否开启使用。GO111MODULE环境变量有三个可能的值:

  • GO111MODULE=off:此值禁用Go模块系统,并改用传统的GOPATH模式。在此模式下,依赖项会下载并存储在GOPATH目录中,而go命令会在GOPATH环境变量指定的目录中查找包。
  • GO111MODULE=on:此值启用Go模块系统,并使用模块来管理依赖关系。在此模式下,go命令会在项目目录中查找go.mod文件,以确定所需的依赖项及其版本。如果文件存在,命令会下载所需的依赖项并将它们存储在本地缓存中,可以在项目之间共享。
  • GO111MODULE=auto:此值在项目目录中存在go.mod文件时启用Go模块系统。如果未找到go.mod文件,则使用传统的GOPATH模式(也就是GO111MODULE=off)。

设置GO111MODULE

通常在运行Go命令之前会设置GO111MODULE作为环境变量。例如,可以这样设置它:

export GO111MODULE=on

或者,可以仅为特定命令设置它:

GO111MODULE=on go build

使用Go模块系统可以让开发人员以更有组织和高效的方式管理项目依赖关系。通过在go.mod文件中指定所需的包和版本,模块系统确保项目可以在不同环境中一致地构建。