Go Module 常规操作

我们已经掌握了 Go Module 构建模式的基本概念和工作原理,也学会了如何通过 go mod 命令,将一个 Go 项目转变为一个 Go Module,并通过 Go Module 构建模式进行构建。

当我们有一个 Go Module 项目之后,就需要考虑是如何维护它,即对 Go Module 依赖包的管理。

添加依赖

在一个项目的初始阶段,我们会经常为项目引入第三方包,并借助这些包完成特定功能。就算项目进入稳定阶段,随着项目演进,我们偶尔也需要在代码中引入新的第三方包。

那我们应该如何为一个 Go Module 添加一个新的依赖包呢?

我们还是以之前的 module-mode 项目为例,为这个项目添加一个新依赖:github.com/google/uuid。

package main

import (
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
)

func main() {
	logrus.Println("hello, go module mode")
	logrus.Println(uuid.NewString())
}
go

新源码中,我们通过 import 语句导入了 github.com/google/uuid,并在 main 函数中调用 uuid 包的函数 NewString。

此时如果我们直接构建这个 module,我们会得到一个错误提示:

go build

main.go:4:2: no required module provides package github.com/google/uuid; to add it:
	go get github.com/google/uuid

Go 编译器提示我们,go.mod 中的 require 段中,并没有提供 gitHub.com/google/uuid 包,我们可以手动执行 go get 命令。

go get github.com/google/uuid

go: downloading github.com/google/uuid v1.3.0
go: added github.com/google/uuid v1.3.0

go get 命令可以将我们新增的依赖包下载到本地的 module 缓存里,并在 go.mod 文件的 require 段中新增一行内容。

module github.com/bigwhite/module-mode

go 1.19

require github.com/sirupsen/logrus v1.9.0

require (
	github.com/google/uuid v1.3.0 // indirect
	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

我们也可以使用 go mod tidy 命令,再执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载它们。

go mod tidy
module github.com/bigwhite/module-mode

go 1.19

require (
	github.com/google/uuid v1.3.0
	github.com/sirupsen/logrus v1.9.0
)

require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

对于我们这个例子而言,手动执行 go get 新增依赖项和执行 go mod tidy 自动分析和下载依赖项的最终效果是等价的。对于复杂项目变更而言,逐一手动添加依赖显然很没有效率,go mod tidy 是更佳的选择。

升级/降级依赖

我们先以对依赖的的版本进行降级为例,进行分析。

在实际开发工作中,如果我们认为 Go 命令自动帮我们确定的某个依赖的版本存在一些问题,比如引入不必要复杂性导致可靠性降低、性能回退等等。我们可以手动将它降级为之前发布的某个兼容版本。

我们还是以上面的 logrus 为例,logrus 存在多个发布版本,我们可以通过下面命令进行查询:

go list -m -versions github.com/sirupsen/logrus

github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1 v1.9.0

在我们的例子中,基于初始状态的 go mod tidy 命令,帮我们选择 logrus 的最新发布版本 v1.9.0。如果你觉得这个版本存在某些问题,想将 logrus 版本降至之前发布的兼容版本,比如 v1.8.0,那么我们可以在项目的 module 根目录下,执行带有版本号的 go get 命令:

go get github.com/sirupsen/logrus@v1.8.0

go: downloading github.com/sirupsen/logrus v1.8.0
go: downloading github.com/stretchr/testify v1.2.2
go: downgraded github.com/sirupsen/logrus v1.9.0 => v1.8.0

从输出结果我们可以看到,go get 命令下载了 logrus v1.8.0 版本,并将 go.mod 中对 logrus 的依赖版本从 v1.9.0 降至 v1.8.0。

当然我们也可以使用 go mod tidy 来帮助我们降级,前提是首先要用 go mod edit 命令,明确告知我们要依赖 v1.8.0 版本,而不是 v1.9.0:

go mod edit -require=github.com/sirupsen/logrus@v1.8.0
go mod tidy

go: downloading github.com/sirupsen/logrus v1.8.0

降级后,我们再假设 logrus v1.8.1 版本是一个安全补丁升级,修复了一个很严重的安全漏洞,并且我们必须要使用这个安全补丁版本,那么我们就需要将 logrus 依赖从 v1.8.0 升级到 v1.8.1。

我们可以使用与降级同样的步骤来完成。

go get github.com/sirupsen/logrus@v1.8.1

到这里你就学会如何对项目依赖包版本进行升级和降级了。

但是你可能会发现一个问题,在前面的例子中,Go Module 的依赖主版本号都是 1。根据我们之前学习的语义导入版本的规范,在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,不需要在包的导入路径上增加版本号,也就是:

import github.com/urer/repo/v0 <=> import github.com/user/repo
import github.com/urer/repo/v1 <=> import github.com/user/repo

但是,如果我们要依赖的 module 的主版本号大于 1,这又要怎么办呢?

语义导入版本有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。也就是说,如果新旧两个包不兼容,那么我们就应该使用不同的导入路径。

按照语义版本规范,如果我们要为项目引入主版本号为 1 的依赖,比如 v2.0.0,那么由于这个版本与 v1、v0 开头的包版本都不兼容,我们在导入 v2.0.0 包时,不能再直接使用 github.com/user/repo,而要使用像下面代码中这样不同的包导入路径。

import github.com/user/repo/v2/xxx

也就是说,如果我们要为 Go 项目添加主版本号大于 1 的依赖,我们就需要使用 “语义导入版本” 机制,在声明它的导入路径的基础上,加上版本信息。

我们以向 module-mode 项目添加 github.com/go-redis/redis 依赖包的 v7 版本 为例,看下添加步骤。

go get github.com/go-redis/redis/v7

go: downloading github.com/go-redis/redis/v7 v7.4.1
go: added github.com/go-redis/redis/v7 v7.4.1

我们可以看到,go get 为我们选择了 go-redis v7 版本下的最新版本 v7.4.1。

移除依赖

我们还是以 go-redis/redis 示例,如果这时我们不再需要 go-redis/redis,应该怎么做?

首先我们可以通过 go list 命令列出当前 module 的所有依赖。

go list -m all

github.com/bigwhite/module-mode
github.com/cespare/xxhash/v2 v2.1.2
github.com/davecgh/go-spew v1.1.1
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
github.com/fsnotify/fsnotify v1.4.9
github.com/go-redis/redis/v7 v7.4.1
github.com/go-redis/redis/v8 v8.11.5
...
gopkg.in/yaml.v2 v2.4.0

我们可以看到 go-redis/redis/v8 出现在结果中。

要想移除 go.mod 的依赖项,我们需要从源码中删除对依赖项的导入语句,然后使用 go mod tidy 命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。go mod tidy 会自动分析源码依赖项,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。

go mod tidy
go list -m all

github.com/bigwhite/module-mode
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.3.0
github.com/pmezard/go-difflib v1.0.0
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.2.2
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8

执行完 go mod tidy 命令后,再次执行 go list -m all 命令,可以看到依赖列表中已经没有 redis 依赖。

特殊情况:使用 vendor

你可能会感觉到有点奇怪,为什么 Go Module 的维护,还需要使用 vendor。

其实,vendor 机制虽然诞生于 GOPATH 构建模式主导的年代,但在 Go Module 构建模式下,它依旧被保留下来,并且成为 Go Module 构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境中, 使用 vendor 机制可以实现与 Go Module 等价的构建。

和 GOPATH 构建模式不同,Go Module 构建模式下,我们无需手动维护 vendor 目录下的依赖包,Go 提供了可以快速建立和更新 vendor 的命令。我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor。

go mod vendor
tree -LF 2 vendor

vendor/
├── github.com/
│   ├── google/
│   └── sirupsen/
├── golang.org/
│   └── x/
└── modules.txt

我们可以看到,go mod vendor 命令在 vendor 目录下,创建了当前项目依赖包的副本,并且通过 vendor/modules.txt. 记录了 vendor 下的 module 以及版本。

如果我们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,我们需要在 go build 后面加上 -mod=vendor 参数。

在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传递 -mod=mod 参数。

总结

在通过 go mod init 为当前 Go 项目创建一个新的 module 后,随着项目的演进,我们在日常开发过程中,会遇到多种常见的维护 Go Module 的场景。

其中最常见的就是为项目添加一个依赖包,我们可以通过 go get 命令手动获取该依赖包的特定版本,更好的方法是通过 go mod tidy 命令让 Go 命令自动取分析新依赖并决定使用新依赖的哪个版本。

  • 通过 go get 我们可以升级或降级某依赖的版本,如果升级或降级前后的版本不兼容,需要修改导入路径中的版本号,这是 Go 语义导入版本机制的要求。
  • 通过 go mod tidy,我们可以自动分析 Go 源码的依赖变更,包括依赖的新增、版本变更以及删除,并更新 go.mod 中的依赖信息。
  • 通过 go mod vendor,我们依旧可以使用 vendor 机制,并且对 vendor 目录下缓存的依赖包进行自动管理。