Go Module使用入门教程(非常详细)
每种编程语言都有包依赖关系,比如,我们使用 Maven 管理 Java 的包依赖,使用 pip 管理 Python 的包依赖,使用 NPM 管理 Node.js 的包依赖。
类库包的管理是评价编程语言成熟度的重要指标之一,Go 语言也不例外。Go语言中包管理的方式经历了多次迭代和选型。在 Go1.5 版本之前,使用 GOROOT 和 GOPATH 这两个环境变量指定包的位置,开发者使用它们解决第三方包的依赖问题。
GOPATH 用于存放第三方包的源码,它是 Go 的工作目录。使用该环境变量的最大缺点就是包依赖管理不足。Go 团队后来意识到包依赖管理的重要性,所以在 Go1.6 到 Go1.9 版本中提供了统一包管理的规范,但是此时又出现了一个新的问题——不再是没有规范,而是规范太多了。这个阶段包管理工具百花齐放,仅官方推荐的就有十多种。
后来在 Go1.11 版本中引入了 Go Module,它是官方提供的包管理工具,它基于 vgo 演变而来,按照版本号进行迭代。
经过两个大版本的迭代,在 Go1.13 版本中 Go Module 已经成熟了。从 Go1.13 版本开始,Go 命令的下载和认证模块都默认使用 Go Module 提供的镜像。
官方对 Go Module 的定位是作为 GOPATH 的替代方案,它支持版本控制和包分发。使用 Go Module 管理包时,不再仅限于在 GOPATH 路径下运行代码。
Go Module 由以下部分组成:
使用 Go Module 时的注意事项如下:
go mod 相关的命令比较多,下面将逐一说明。
我们可以使用 go help mod 命令获取关于 Go Module 的帮助信息。如果该命令下面还有子命令,如 edit 命令,则可以使用 go help mod edit 命令获取进一步的帮助信息。
go mod 相关的命令如下表所示:
go mod init 命令会生成 go.mod 文件,go.mod 文件的内容完全由 Go 工具链控制。Go 工具链在执行各类命令(如 go get、go build、go mod)时会自动维护 go.mod 文件。
我们在编写“hello world”程序时,已经使用了 Go Module。在安装好 Go环境后,先使用命令 go mod init golang-1 初始化模块,初始化完成后,在目录中创建了 go.mod 文件,该文件中的内容是“module golang-1”,以及对应的 Go版本(Go1.18)。
接下来,我们将演示 Go Module 如何下载 echo 包及其依赖包,具体代码如下:
这时执行 go run main.go 命令,如果 echo 包及其依赖包未下载,go mod 工具链会自动检测并下载这些包:
使用 curl 命令测试,成功得到如下输出:
查看 go.mod 文件内容的变化:
Go Module 安装第三方包的原则是首先拉取最新的 release tag,如果没有 tag,则拉取最新的提交(commit)。Go Module 会自动生成 go.sum 文件,用于记录依赖树。
go.mod 文件必须提交到 Git 仓库中,而 go.sum 文件则不必提交到 Git 仓库中。
再次尝试执行 go run main.go 命令,发现跳过了检查和安装依赖的步骤:
也可以使用 go mod download 命令手动下载你所需要的依赖包。默认情况下,此包会被下载到目录 $GOPATH/pkg/mod 中。
如果 go.mod 文件中引用了某些包,但这些包并没有下载到当前模块的缓存中,go mod tidy 命令则会自动下载这些包。如果 go.mod 文件中记录了一些包,但是项目的源码实际上并没有使用这些包,go mod tidy 命令则会从 go.mod 中移除这些无用的记录信息。
go mod tidy 命令还会更新 go.sum 文件,确保它包含所有依赖项的正确版本。
下面尝试对 go.mod 中某个依赖包的版本进行修改,并查看验证的结果。
① 将包 echo 的版本号从 v3 修改为 v300,命令如下:
② 执行 verify 命令,检查结果如下:
③ 将依赖包的版本从 v300 改回 v3,再重新验证:
比如想知道 github.com/ labstack/echo 这个包在哪些地方被引用了,可以采用如下命令:
使用 go mod vendor 命令后,下面测试一下工程和依赖包是否仍依赖 $GOPATH 目录:
首先,将 $GOPATH/pkg/mod/github.com 下的依赖包删除:
再次到工程目录下对 main.go 进行编译并运行,看是否报错:
② 将某个依赖包的某个版本排除在外。在某些情况下,下载依赖包时,我们可能想将其中某个版本的包排除在外,此时可以使用 go mod edit -excludee=path@version 命令。
例如,使用选项 -exclude 将 canal-go@v1.0.8 版本排除在外后,就可以看到 go.mod 文件中多了一条记录 exclude github.com/ withlin/canal-go v1.0.8。这之后使用 go get 命令下载此版本的包,就会提示此版本的包是被排除的包,因此不会被下载下来。
③ 替换依赖包的某个版本
如果想替换依赖包的版本,可以使用 go mod edit -replace=old[@v]=new[@v]命令。
比如,要将 canal-go 的版本从 1.0.9 改为 1.0.7,可执行如下命令:
执行替换命令后,可以看到 go.mod 文件中多了一行以 replace 开头的记录:
go.mod 文件提供了四个命令,分别是 module、require、replace 和 exclude:
假设某个包 X 的最新版本是 v1.0.3。A 模块使用了包 X 的 v1.0.1 版本,B 模块使用了包 X 的 v1.0.2 版本,那么来看看在下表所列的几种场景下,如何选择包 X 的版本。
下面说明一下使用不同版本号的原因:
所以,像 Kubernetes 这类项目,我们可以看到它导入包的语句为:

图 1 Go Module的使用总结
类库包的管理是评价编程语言成熟度的重要指标之一,Go 语言也不例外。Go语言中包管理的方式经历了多次迭代和选型。在 Go1.5 版本之前,使用 GOROOT 和 GOPATH 这两个环境变量指定包的位置,开发者使用它们解决第三方包的依赖问题。
GOPATH 用于存放第三方包的源码,它是 Go 的工作目录。使用该环境变量的最大缺点就是包依赖管理不足。Go 团队后来意识到包依赖管理的重要性,所以在 Go1.6 到 Go1.9 版本中提供了统一包管理的规范,但是此时又出现了一个新的问题——不再是没有规范,而是规范太多了。这个阶段包管理工具百花齐放,仅官方推荐的就有十多种。
后来在 Go1.11 版本中引入了 Go Module,它是官方提供的包管理工具,它基于 vgo 演变而来,按照版本号进行迭代。
经过两个大版本的迭代,在 Go1.13 版本中 Go Module 已经成熟了。从 Go1.13 版本开始,Go 命令的下载和认证模块都默认使用 Go Module 提供的镜像。
官方对 Go Module 的定位是作为 GOPATH 的替代方案,它支持版本控制和包分发。使用 Go Module 管理包时,不再仅限于在 GOPATH 路径下运行代码。
Go Module 由以下部分组成:
- 集成到 Go命令中的工具集,如 go mod xxx。
- go.mod 文件,用于保存所有的依赖列表。
- go.sum 文件,指与版本相关的管理文件,用于保存不同的版本信息以验证依赖的散列值,防止恶意修改。与 go.mod 文件一样,此文件通常不需要手动修改,它会通过执行与 go mod 相关的命令自动修改。
开启Go Module
在 Go1.11 和 Go1.12 版本中,默认情况下不开启 Go Module,在 Go1.13 版本中 Go Module 默认处于开启状态。可以通过设置环境变量 GO111MODULE 切换开启或关闭的状态。使用 Go Module 时的注意事项如下:
- 使用 Go Module 后,项目存放位置不一定在 $GOPATH/src 目录中。
- 执行 go build 或 go run 命令时,Go 会自动拉取本地没有但导入了的包。
- 启用 Go Module 后,首先会从 $GOROOT 中查找包,如果找不到,则根据项目的 go.mod 文件在 $GOPATH/pkg/mod 中查找,如果还是没有找到或包的版本号不正确,则会自动拉取,若拉取失败则报错。
使用Go Module
Go Module 使用 go mod 的一系列命令来管理包和包的依赖,并自动维护两个相关文件 go.mod 和 go.sum。go mod 相关的命令比较多,下面将逐一说明。
我们可以使用 go help mod 命令获取关于 Go Module 的帮助信息。如果该命令下面还有子命令,如 edit 命令,则可以使用 go help mod edit 命令获取进一步的帮助信息。
go mod 相关的命令如下表所示:
命令 | 说明 |
---|---|
init | 在当前目录下初始化模块,生成 go.mod 文件 |
graph | 输出模块依赖图 |
download | 下载相关包及其依赖包 |
tidy | 添加需要的依赖,删除无用的依赖 |
verify | 校验依赖的内容和格式,也可以校验依赖的源码是否被修改 |
why | 输出依赖关系 |
vendor | 将所有的依赖复制到当前的 vendor 目录中 |
edit | 编辑依赖,下面有多个子命令 |
go mod init 命令会生成 go.mod 文件,go.mod 文件的内容完全由 Go 工具链控制。Go 工具链在执行各类命令(如 go get、go build、go mod)时会自动维护 go.mod 文件。
1) 初始化命令go mod init
在项目目录中使用 go mod init 命令初始化模块名时会生成 go.mod 文件。因为模块不需要包含如 src、bin 等的子目录,所以即使项目目录是空目录,也可以作为模块,只要其中包含了 go.mod 文件。我们在编写“hello world”程序时,已经使用了 Go Module。在安装好 Go环境后,先使用命令 go mod init golang-1 初始化模块,初始化完成后,在目录中创建了 go.mod 文件,该文件中的内容是“module golang-1”,以及对应的 Go版本(Go1.18)。
接下来,我们将演示 Go Module 如何下载 echo 包及其依赖包,具体代码如下:
$ cat ../intro-golang/use3rdpkg/main.go package main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "hello World!") }) e.Logger.Fatal(e.Start(":8888")) }
这时执行 go run main.go 命令,如果 echo 包及其依赖包未下载,go mod 工具链会自动检测并下载这些包:
$ go run main.go go: downloading github.com/labstack/echo v3.3.10+incompatible ... ... ⇨ http server started on [::]:8888
使用 curl 命令测试,成功得到如下输出:
$ curl http://localhost:8888/ hello World!
查看 go.mod 文件内容的变化:
$ cat go.mod module golang-1 go 1.18 require ( github.com/labstack/echo v3.3.10+incompatible // indirect github.com/labstack/gommon v0.3.0 // indirect golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect )这时发现 go.mod 文件中多了 github.com/labstack/echo、echo 依赖的 github.com/ labstack/gommon v0.3.0 和 golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee。
Go Module 安装第三方包的原则是首先拉取最新的 release tag,如果没有 tag,则拉取最新的提交(commit)。Go Module 会自动生成 go.sum 文件,用于记录依赖树。
go.mod 文件必须提交到 Git 仓库中,而 go.sum 文件则不必提交到 Git 仓库中。
再次尝试执行 go run main.go 命令,发现跳过了检查和安装依赖的步骤:
$ go run main.go ⇨ http server started on [::]:8888
2) 使用go mod graph命令输出依赖包
go mod graph 命令会按照如下格式打印输出模块中依赖包的信息:$ pwd .../intro-golang/use3rdpkg $ go mod graph golang-1 github.com/labstack/echo@v3.3.10+incompatible golang-1 github.com/labstack/gommon@v0.3.0 golang-1 golang.org/x/crypto@v0.0.0-20201012173705-84dcc777aaee ... ... github.com/stretchr/testify@v1.4.0 github.com/stretchr/objx@v0.1.0 github.com/stretchr/testify@v1.4.0 gopkg.in/yaml.v2@v2.2.2 gopkg.in/yaml.v2@v2.2.2 gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405
3) 使用go mod download命令下载依赖包
如果你从 GitHub 上拉取了第三方包,且拉取的包中已经有了 go.mod 文件,那么 Go 会首先在目录 $GOPATH/pkg/mod 中查找是否存在第三方包需要的依赖包,没有就下载。也可以使用 go mod download 命令手动下载你所需要的依赖包。默认情况下,此包会被下载到目录 $GOPATH/pkg/mod 中。
4) 使用go mod tidy命令整理依赖
go mod tidy 是一个很有用的命令,它会根据 go.mod 文件中的内容整理当前 Go 模块的依赖关系。如果 go.mod 文件中引用了某些包,但这些包并没有下载到当前模块的缓存中,go mod tidy 命令则会自动下载这些包。如果 go.mod 文件中记录了一些包,但是项目的源码实际上并没有使用这些包,go mod tidy 命令则会从 go.mod 中移除这些无用的记录信息。
go mod tidy 命令还会更新 go.sum 文件,确保它包含所有依赖项的正确版本。
5) 使用go mod verify命令验证依赖
go mod verify 命令会验证 go.mod 文件中的依赖是否存在,并校验依赖的源码是否被修改。下面尝试对 go.mod 中某个依赖包的版本进行修改,并查看验证的结果。
① 将包 echo 的版本号从 v3 修改为 v300,命令如下:
$ diff go.mod go.mod.bak 6c6 < github.com/labstack/echo v300.3.10+incompatible // indirect --- > github.com/labstack/echo v3.3.10+incompatible // indirect
② 执行 verify 命令,检查结果如下:
$ go mod verify go: github.com/labstack/echo@v300.3.10+incompatible: reading github.com/labstack/echo/go.mod at revision v300.3.10: unknown revision v300.3.10
③ 将依赖包的版本从 v300 改回 v3,再重新验证:
$ go mod verify all modules verified可以看到重新验证后再没有报错出现。
6) 使用go mod why命令查看依赖包的依赖关系
可以使用 go mod why 命令查看某个依赖包的依赖关系。比如想知道 github.com/ labstack/echo 这个包在哪些地方被引用了,可以采用如下命令:
$ cd $GOPATH/src/golang-1 $ go mod why -m github.com/labstack/echo # github.com/labstack/echo golang-1/intro-golang/use3rdpkg github.com/labstack/echo可以看到,最后的输出结果显示是在 golang-1/intro-golang/use3rdpkg 中引用了这个 echo 包。
7) 下载并复制依赖包到工程目录下的vender目录中
使用 go mod vendor 命令可将依赖包下载并复制到工程目录下的 vender 目录中。执行该命令后,首先会在工程目录下创建一个 vendor 目录,然后会把 go.mod 文件中的依赖包同步到本地的 vendor 目录中。$ go mod vendor $ ls vendor github.com go.opentelemetry.io golang.org google.golang.org gopkg.in modules.txt
使用 go mod vendor 命令后,下面测试一下工程和依赖包是否仍依赖 $GOPATH 目录:
首先,将 $GOPATH/pkg/mod/github.com 下的依赖包删除:
$ pwd /Users/makesure10/Desktop/goworkspace/pkg/mod/ $ rm -rf github.com
再次到工程目录下对 main.go 进行编译并运行,看是否报错:
$ cd $GOPATH/src/golang-1/intro-golang/use3rdpkg $ go run main.go ⇨ http server started on [::]:8888最终结果表明,使用 go mod vendor 命令后,工程和依赖包都不再依赖 $GOPATH 目录。这样做的好处是,可以轻松将工程打包,而无须下载包所涉及的网络、版本等内容;弊端是会存在多份依赖包备份,形成冗余。
8) 使用go mod edit命令添加、排除、替换依赖包
① 添加某个依赖包。go mod edit -require 命令会自动将所有需要的依赖包添加到 go.mod 文件中。② 将某个依赖包的某个版本排除在外。在某些情况下,下载依赖包时,我们可能想将其中某个版本的包排除在外,此时可以使用 go mod edit -excludee=path@version 命令。
例如,使用选项 -exclude 将 canal-go@v1.0.8 版本排除在外后,就可以看到 go.mod 文件中多了一条记录 exclude github.com/ withlin/canal-go v1.0.8。这之后使用 go get 命令下载此版本的包,就会提示此版本的包是被排除的包,因此不会被下载下来。
③ 替换依赖包的某个版本
如果想替换依赖包的版本,可以使用 go mod edit -replace=old[@v]=new[@v]命令。
比如,要将 canal-go 的版本从 1.0.9 改为 1.0.7,可执行如下命令:
$ go mod edit -replace=github.com/withlin/canal-go@1.0.9=github.com/withlin/canal-go@1.0.7
执行替换命令后,可以看到 go.mod 文件中多了一行以 replace 开头的记录:
$ cat go.mod module golang-1 go 1.17 require ( ... github.com/withlin/canal-go v1.0.9 ... ) exclude github.com/withlin/canal-go v1.0.8 replace github.com/withlin/canal-go 1.0.9 => github.com/withlin/canal-go 1.0.7
go.mod文件中的命令
通常来说,在执行 go mod 相关的命令时会自动维护 go.mod 和 go.sum 这两个文件。go.mod 文件提供了四个命令,分别是 module、require、replace 和 exclude:
- module:指定模块的名字(路径);
- require:添加依赖包,指定最低版本要求;
- replace:替换依赖包,手动指定依赖包(可以替换全部的版本、指定的版本或本地的版本等);
- exclude:忽略依赖包。
升级依赖包的方法
我们可以使用工具链来确定可以升级的依赖包,相应的命令及作用如下表所示:步骤 | 使用的命令 | 作用 |
---|---|---|
1 | go get -u 需要升级的包名 | 升级后会将新的依赖版本更新到 go.mod 文件中 |
2 | go list -m all | 列出所有将在 go build 中使用的模块和它们的具体版本号 |
3 | go list -u -m all | 列出使用的各个模块目前可用的小更新或者补丁版本号 |
4 | go get -u or go -u=patch | 将依赖所有直接或间接模块的版本号更新为最新可用的补丁版本号 |
5 | go mod tidy | 从 go.mod 文件中删除目前已经不再使用的依赖模块,并加入其他操作系统和架构所需的依赖 |
6 | go mod vendor | 将所有依赖放入当前模块的 vendor 目录中 |
7 | go build -mod vendor | 使用当前模块 vendor 目录中的依赖代码(不是缓存中依赖模块的代码)进行编译、构建 |
依赖包版本的选择
每个包管理方案都需要解决“依赖版本”问题。大多数的版本选择算法都是想要识别所有依赖的最新版本,但 Go Module 是选择“项目中最适合的最低版本”。假设某个包 X 的最新版本是 v1.0.3。A 模块使用了包 X 的 v1.0.1 版本,B 模块使用了包 X 的 v1.0.2 版本,那么来看看在下表所列的几种场景下,如何选择包 X 的版本。
序号 | 场景描述 | 版本选择 |
---|---|---|
场景一 | 项目中使用了 A 模块 | 在 dep 中采用语义版本控制,所以在这种情况下会选择包 X 的 v1.0.3 版本。在 Go Module 中采用最低版本控制,所以在这种情况下会选择包 X 的 v1.0.1 版本 |
场景二 | 项目中使用了 A 模块和 B 模块 | 选择的包 X 的版本为 v1.0.2 |
场景三 | 删除了 B 模块,只保留 A 模块 | 此时选择的包 X 的版本为 v1.0.2,不会降级为 A 模块所用的 v1.0.1 版本。因为 Go 认为降级是一个更大的更改,v1.0.2 这个版本已经可以稳定运行了,那么默认 v1.0.2 是最低版本 |
Go Module语义版本的导入路径语法
Go Module 使用了一种语义版本的导入路径语法。每个版本的命名规范是“v主版本号.次版本号.修订版本号”。下面说明一下使用不同版本号的原因:
- 主版本号:有较大的更新,导致 API 与早期的版本不兼容;
- 次版本号:增加了向下兼容的新特性;
- 修订版本号:进行了向下兼容的补丁修复。
所以,像 Kubernetes 这类项目,我们可以看到它导入包的语句为:
import "github.com/ kubernetes/ kubernetes/v主版本号/次版本号/包名"
Go Module的使用总结
最后,我们对 Go Module 的使用进行了总结,如下图所示:
图 1 Go Module的使用总结