go module常规操作
# 7. Go Module常规操作
维护Go Module的六个场景:
- 为当前 module 添加一个依赖
- 升级 / 降级依赖的版本
- 添加一个主版本号大于 1 的依赖
- 升级依赖版本到一个不兼容版本
- 移除一个依赖
- 特殊情况:使用 vendor
# 7.1 为当前 module 添加一个依赖
在一个项目的初始阶段,我们会经常为项目引入第三方包,并借助这些包完成特定功能。即便是项目进入了稳定阶段,随着项目的演进,我们偶尔还需要在代码中引入新的第三方包。
例如增加一个新依赖:github.com/google/uuid
首先会更新源码main.go:
package main
import (
"github.com/google/uuid" //导入包
"github.com/sirupsen/logrus"
)
func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString()) //调用包函数
}
2
3
4
5
6
7
8
9
10
11
此时,如果我们直接构建这个 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
2
3
提示没找到包,对于没有的包,有两种方式添加:
第一种:go get
第二种:go mod tidy
第一种:使用 go get 命令:
$go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go get: added github.com/google/uuid v1.3.0
2
3
执行go get 命令后,会将我们新增的依赖包下载到了本地 module 缓存里,并在 go.mod 文件的 require 段中新增了一行内容:
require (
github.com/google/uuid v1.3.0 //新增的依赖
github.com/sirupsen/logrus v1.8.1
)
2
3
4
第二种:使用 go mod tidy 命令:
$go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.3.0
2
3
执行go mod tidy 命令,在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载它们。
go get 和 go mod tidy 区别:
手工执行 go get 新增依赖项,和执行 go mod tidy 自动分析和下载依赖项的最终效果,是等价的。但对于复杂的项目变更而言,逐一手工添加依赖项显然很没有效率,go mod tidy 是更佳的选择。
# 7.2 升级/降级依赖的版本
在实际开发工作中,如果我们认为 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
2
基于初始状态执行的 go mod tidy 命令,帮我们选择了 logrus 的最新发布版本 v1.8.1,如果你觉得这个版本存在某些问题,想将 logrus 版本降至某个之前发布的兼容版本,比如 v1.7.0,有两种操作方法:
第一种:在项目的 module 根目录下,执行带有版本号的 go get 命令:
$go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0
2
3
第二种:在项目的 module 根目录下,执行go mod tidy 来帮助我们降级:
前提:首先要用 go mod edit 命令,明确告知我们要依赖 v1.7.0 版本,而不是 v1.8.1
$go mod edit -require=github.com/sirupsen/logrus@v1.7.0
$go mod tidy
go: downloading github.com/sirupsen/logrus v1.7.0
2
3
升级操作与降级操作类似,指定升级的版本号就可以了。
# 7.3 添加一个主版本号大于 1 的依赖
在 Go Module 构建模式下,当依赖的主版本号为 0 或 1 的时候,我们在 Go 源码中导入依赖包,不需要在包的导入路径上增加版本号:
import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo
2
但是,如果我们要依赖的 module 的主版本号大于 1,这又要怎么办呢?
语义导入版本机制有一个原则:如果新旧版本的包使用相同的导入路径,那么新包与旧包是兼容的。
也就是说,如果新旧两个包不兼容,那么我们就应该采用不同的导入路径。
例如在导入v2.0.0时,需要使用带版本号的包导入路径,在声明它的导入路径的基础上,加上版本号信息:
import github.com/user/repo/v2/xxx
添加 github.com/go-redis/redis 依赖包的 v7 版本为例,以空导入的方式导入 v7 版本的 github.com/go-redis/redis 包:
package main
import (
_ "github.com/go-redis/redis/v7" //“_”为空导入
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
func main() {
logrus.Println("hello, go module mode")
logrus.Println(uuid.NewString())
}
2
3
4
5
6
7
8
9
10
11
12
空导入:空导入也是导入,例如import _ "foo",意味着我们将依赖foo这个路径下的包。但由于是空导入,我们并没有显式使用这个包中的任何语法元素。那么空导入的意义是什么呢?由于依赖foo包,程序初始化的时候会沿着包的依赖链初始化foo包,包的初始化会按照常量->变量->init函数的次序进行。通常实践中空导入意味着期望依赖包的init函数得到执行,这个init函数中有我们需要的逻辑。
接下来的步骤就与添加兼容依赖一样,我们通过 go get 获取 redis 的 v7 版本:
$go get github.com/go-redis/redis/v7
go: downloading github.com/go-redis/redis/v7 v7.4.1
go: downloading github.com/go-redis/redis v6.15.9+incompatible
go get: added github.com/go-redis/redis/v7 v7.4.1
2
3
4
go get 为我们选择了 go-redis v7 版本下当前的最新版本 v7.4.1
# 7.4 升级依赖版本到一个不兼容版本
有些时候,出于要使用依赖包最新功能特性等原因,我们可能需要将某个依赖的版本升级为其不兼容版本,也就是主版本号不同的版本,这又该怎么做呢?
还以 go-redis/redis 这个依赖为例,将这个依赖从 v7 版本升级到最新的 v8 版本:
首先将代码中 redis 包导入路径中的版本号改为 v8:
import (
_ "github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
2
3
4
5
再通过 go get 来获取 v8 版本的依赖包:
$go get github.com/go-redis/redis/v8
go: downloading github.com/go-redis/redis/v8 v8.11.1
go: downloading github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: downloading github.com/cespare/xxhash/v2 v2.1.1
go get: added github.com/go-redis/redis/v8 v8.11.1
2
3
4
5
go get 为我们升级到了 go-redis v8 版本下当前的最新版本 v8.11.1
# 7.5 移除一个依赖
如果我们不需要再依赖 go-redis/redis 了,需要移除这个包。
你可能会删除掉代码中对 redis 的空导入这一行,之后再利用 go build 命令成功地构建这个项目。但你会发现,与添加一个依赖时 Go 命令给出友好提示不同,这次 go build 没有给出任何关于项目已经将 go-redis/redis 删除的提示,并且 go.mod 里 require 段中的 go-redis/redis/v8 的依赖依旧存在着。
其实,要想彻底从项目中移除 go.mod 中的依赖项,仅从源码中删除对依赖项的导入语句还不够。这是因为如果源码满足成功构建的条件,go build 命令是不会“多管闲事”地清理 go.mod 中多余的依赖项的。
需要用 go mod tidy 命令,将这个依赖项彻底从 Go Module 构建上下文中清除掉。
go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。
# 7.6 特殊情况:使用 vendor
作为 Go Module 构建机制一个很好的补充,在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境 (CI/CD) 中,使用 vendor 机制可以实现与 Go Module 等价的构建。
和 GOPATH 构建模式不同,Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:
$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│ ├── google/
│ ├── magefile/
│ └── sirupsen/
├── golang.org/
│ └── x/
└── modules.txt
2
3
4
5
6
7
8
9
10
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 的参数。
# 7.7 其他场景
- 通过 go get 我们可以升级或降级某依赖的版本,如果升级或降级前后的版本不兼容,这里千万注意别忘了变化包导入路径中的版本号,这是 Go 语义导入版本机制的要求;
- 通过 go mod tidy,我们可以自动分析 Go 源码的依赖变更,包括依赖的新增、版本变更以及删除,并更新 go.mod 中的依赖信息。
- 通过 go mod vendor,我们依旧可以支持 vendor 机制,并且可以对 vendor 目录下缓存的依赖包进行自动管理。