
01
介绍
Go 自 1.11 以来,包含对 Module 版本的支持。初始原型 vgo 于 2018 年 2 月宣布。2018 年 7 月,Module 版本进入 Go 代码仓库主分支。
Go 1.11 和 1.12 包含对 Go Modules 的初步支持,Go 的新依赖项管理系统使依赖关系版本信息更加明确且更易于管理。

图片来自https://www.callicoder.com/golang-packages/
Module 是存储在文件树中的 Go 包的集合,其根目录有 go.mod 文件。go.mod 文件定义了 Module 的模块路径,该路径也是用于根目录的导入路径,以及其依赖项要求,这些依赖项要求是成功构建所需的其他模块。每个依赖项要求都编写为模块路径和特定的语义版本。
自 Go 1.11 起,可以显式启用模块模式(通过设置 GO111MODULE=on),go 命令允许在当前目录或任何父目录具有 go.mod 文件时使用模块模式,前提是该目录位于 GOPATH/src 之外。(GOPATH/src 内部,为了兼容性,go 命令仍以旧的 GOPATH 模式运行,即使找到 go.mod 文件。
在 Go 1.13,无需显式设置启用模块模式,只需设置 GO111MODULE=auto,如果发现任何 go.mod,即使在 GOPATH 内部,也表示启用模块模式。
(在 Go 1.13 之前,GO111MODULE=auto 永远不会在 GOPATH 内启用模块模式)。
自 Go 1.14 以来,模块支持被视为可供生产环境使用,鼓励所有用户从其他依赖管理系统迁移到 Module。并且改回需显式设置启用模块模式
(通过设置 GO111MODULE=on),如果不存在 go.mod 文件,大多数模块命令的功能更有限。
在 Go 1.15,可以通过 GOMODCACHE 环境变量设置模块缓存的位置。GOMODCACHE 的默认值是 GOPATH[0]/pkg/mod,可以在此更改模块缓存的位置。
本文将学习使用模块开发 Go 代码时出现的一系列基本操作,示例假定在 Linux 系统中 gopher 用户的家目录。
02
创建一个新 Module
在 $GOPATH/src 外的任何位置,创建一个新的空目录,进入新创建的目录,创建一个新的源码文件:hello.go。
package hello
func Hello() string {
return "Hello, world."
}再创建一个测试源码文件:hello_test.go。
package hello
import "testing"
func TestHello(t *testing.T) {
want := "Hello, world."
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}此时,目录包含包,但不包含模块,因为没有 go.mod 文件。如果我们在 /home/gopher/hello 目录去运行测试命令 go test,我们将看到:
$ go test
PASS
ok _/home/gopher/hello 0.020s
$因为我们在 $GOPATH 之外,并且在任何模块之外的目录运行,go 命令不知道当前目录的导入路径,根据目录名称组成一个假的导入路径:_/home/gopher/hello。
让我们使用 go mod init 使当前目录成为模块的根目录,然后重试测试:
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok example.com/hello 0.020s
$祝贺!您已经编写并测试了第一个模块。go mod init 命令写了一个 go.mod 文件:
$ cat go.mod
module example.com/hello
go 1.12
$go.mod 文件仅出现在模块的根目录中。子目录中的包具有导入路径,包括模块路径和子目录路径。例如,如果我们创建了一个子目录 world,
我们不需要(也不想)运行 go mod init。包将自动识别为该模块 example.com/hello 的一部分,导入路径 example.com/hello/world。
03
添加依赖项
Go modules 的主要目的是改进使用其他开发人员编写的代码(即添加依赖项)的体验。
让我们更新我们的 hello.go 导入 rsc.io/quote 并用它来实现函数 Hello:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}现在让我们再次运行 go test:
$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok example.com/hello 0.023s
$go test 命令使用 go.mod 文件中列出的特定依赖项模块版本解析导入。当它遇到 go.mod 文件中任何模块未提供的包的导入时,go 命令会自动通过「最新版本」来备份包含该包的模块并将其添加到 go.mod。「最新」定义为最新的标记稳定(非预发行)版本,或者最新的标记预发行版本,或者最新的未标记版本。
在我们的示例中,go test 导入 rsc.io/quote 模块的最新版本 rsc.io/quote v1.5.2。
它还下载了两个间接依赖项,
rsc.io/sampler 和 golang.org/x/text。
仅将直接依赖项记录在 go.mod 文件中:
$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
$再次运行 go test 命令,不会重复下载检索工作,因为 go.mod 现在是最新的,下载的模块在本地缓存目录中($GOPATH[0]/pkg/mod):
$ go test
PASS
ok example.com/hello 0.020s
$请注意,虽然 go 命令使添加新的依赖项变得快速而简单,但它并非没有成本。您的模块现在实际上依赖于关键领域(如正确性、安全性和正确许可等)中的新依赖关系。
正如我们上面看到的,添加一个直接依赖关系通常也会带来其他间接依赖关系。命令 go list -m all 列出了当前模块及其所有依赖项:
$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$在 go 列表输出中,当前模块(也称为主模块)始终是第一行,后跟按模块路径排序的依赖项。
golang.org/x/text 版本 v0.0.0-20170915032832-14c0d48ead0c 是伪版本的示例,这是特定未标记提交的命令版本语法。
除了 go.mod 之外,go 命令还维护一个名为 go.sum 的文件,其中包含特定模块版本内容的预期加密哈希:
$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$go 命令使用 go.sum 文件来确保这些模块的未来下载检索与第一次下载相同的位,以确保项目所依赖的模块不会意外更改,无论是出于恶意、意外还是其他原因。go. mod 和 go. sum 都应签入版本控制。
04
升级依赖项
使用 Go modules,版本使用语义版本标记进行引用。语义版本由三个部分组成:主要版本、次要版本和修补程序版本。例如,对于 v0.1.2,主要版本为 0,次要版本为 1,修补程序版本为 2。让我们演练几个次要版本升级。
从 go list -m all
的输出中,我们可以看到我们使用的是未标记的 golang.org/x/text。让我们升级到最新的标记版本,并测试一切仍然有效:
$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok example.com/hello 0.013s
$运行成功,让我们再看看 go list -m all 和 go.mod 文件:
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
)
$golang.org/x/text 已升级到最新的标记版本 (v0.3.0)。go.mod 文件已更新,指定 v0.3.0。注释「indirect」指示依赖项不直接由此模块使用,仅由其他模块依赖项间接使用。
现在,让我们尝试升级 rsc.io/sampler 版本。通过运行 go get 和运行 go test:
$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL example.com/hello 0.014s
$go test 运行失败表明最新版本的 rsc.io/sampler 与我们的用法不兼容。让我们列出该模块的可用标记版本:
$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$我们一直在使用v1.3.0,v1.99.99 明显不兼容。也许我们可以尝试使用 v1.3.1 代替:
$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok example.com/hello 0.022s
$
请注意在 go get 中的显式内容 @v1.3.1 指定 Module 版本。通常,传递给 go get 的每个参数都可以获取显式版本,默认值为@latest,解析为前面定义的最新版本。
05
添加对新的主版本的依赖
让我们在我们的包中添加一个新函数:func Proverb 返回 Go 并发原语,通过调用 quote.Concurrency,
由模块 rsc.io/quote/v3 提供。首先我们更新 hello.go 添加新函数:
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
然后,我们添加一个 hello_test.go:
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q, want %q", got, want)
}
}然后,我们可以测试我们的代码:
$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok example.com/hello 0.024s
$
请注意,我们的模块现在依赖于 rsc.io/quote 和 rsc.io/quote/v3:
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$
Go modules 的每个不同主要版本(v1、v2 等)使用不同的模块路径:从 v2 开始,路径必须以主要版本结束。
在示例中,
rsc.io/quote的 v3 版本不再 rsc.io/quote:而是由模块路径 rsc.io/quote/v3。
此约定称为语义导入版本控制,它为不兼容的包(具有不同主要版本的包)提供不同的名称。
相比之下,
rsc.io/quote 的 v1.6.0 应与 v1.5.2 向后兼容,因此它重用 rsc.io/quote。(在上一节中,rsc.io/sampler v1.99.99 应与 rsc.io/sampler v1.3.0 向后兼容,但模块行为的错误或不正确的客户端假设都可能发生。
go 命令允许生成最多包含任何特定模块路径的一个版本,这意味着最多包含每个主要版本的一个版本:一个 rsc.io/quote、一个 rsc.io/quote/v2、rsc.io/quote/v3,等等。这为模块作者提供了关于单个模块路径可能重复的清晰规则:程序不可能同时使用 rsc.io/quote v1.5.2 和 rsc.io/quote v1.6.0 构建。同时,允许模块的不同主要版本(因为它们具有不同的路径)使模块使用者能够逐步升级到新的主要版本。
在此示例中,我们希望使用 rsc/quote/v3 v3.1.0 的 quote.Concurrency,但尚未准备好迁移我们使用 rsc.io/quote v1.5.2。我们暂且通过起别名的方式使用。在大型程序或代码库中,增量迁移的能力尤为重要。
06
将依赖项升级到新的主版本
让我们完成从同时使用 rsc.io/quote 和 rsc.io/quote/v3
到仅使用 rsc.io/quote/v3 的依赖项升级。由于主要版本更改,我们预期某些 API 可能已被删除、重命名或以其他方式以不兼容的方式更改。
运行 go doc rsc.io/quote/v3 命令,
阅读文档, 我们可以看到, Hello() 已成为 Hellov3():
$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$(输出中还有一个已知错误,显示的导入路径错误地丢弃了 /v3。)
我们可以更新在 hello.go 中使用的 quote.Hello(),改为使用 quoteV3.HelloV3():
package hello
import quoteV3 "rsc.io/quote/v3"
func Hello() string {
return quoteV3.HelloV3()
}
func Proverb() string {
return quoteV3.Concurrency()
}现在仅使用依赖项 rsc.io/quote 的一个版本,不再需要给导入的依赖项定义别名 quoteV3,因此我们可以修改为:
package hello
import "rsc.io/quote/v3"
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}让我们重新运行 go test,以确保一切正常运行:
$ go test
PASS
ok example.com/hello 0.014s07
删除未使用的依赖项
我们已经删除了我们使用的依赖项 rsc.io/quote, 但它仍然出现在 go list -m all 和我们的 go.mod 文件中:
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.0.0
rsc.io/sampler v1.3.1 // indirect
)
$为什么?因为构建单个包(如 go build 或 go test)可以轻松地判断何时缺少依赖项并需要添加,但何时可以安全地删除依赖项,只有在检查了模块中的所有包以及这些包的所有可能的生成标记组合后,才能删除依赖项。普通构建命令不加载此信息,因此无法安全地删除依赖项。
但是,我们可以使用 go mod tidy 命令清理这些未使用的依赖项:
$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
$ go test
PASS
ok example.com/hello 0.020s
$08
总结
Go modules 是 Go 未来版本中使用的依赖项管理系统。模块功能现在在所有支持的 Go 版本中可用(即从 Go 1.11 起的版本)。
本文介绍了 Go modules 这些基本操作:
建议大家开始在本地开发中使用 module,并将 go.mod 和 go.sum 文件添加到项目中的版本控制。
参考资料:
https://blog.golang.org/using-go-modules
https://github.com/golang/go/wiki/Modules
题图来源:
https://blog.magnussen.casa/post/a-primer-to-go-modules/
https://www.callicoder.com/golang-packages/