- 原文标题:Using Go Modules
- 原文作者:Tyler Bui-Palsulich and Eno Compton
- 原文时间:2019-03-19
Go 1.11 和 1.12 版本初步支持了 模块(modules)。它是 Go 中新的依赖管理系统,能更简单的显示、管理依赖的版本。本文介绍将模块
使用入门所需要的一些基本操作。后续将有文章来涵盖跟多模块
的使用。
一个模块
就是一系列 Go packages
的集合,其存储在一个根目录中包含 go.mod
文件的文件夹中。go.mod
文件定义了模块的 模块路径
,这也是导入路径中的根路径,还定义了能成功构建本模块所需的依赖模块。每一项依赖表示为一个模块路径和一个特殊的语义化的版本号。
Go 1.11 中,go 会在下面情况下使用模块
:当前目录或任何上级目录中有 go.mod
文件,且在 $GOPATH/src
目录之外(为了兼容,在 $GOPATH/src
目录中依然使用老的 GOPATH 模式,即使存在 go.mod
,详情见 go 命令文档)。从 Go 1.13 开始,模块模式
将成为默认的依赖管理系统。
本文介绍使用模块
来开发 Go 项目时一些常用操作:
- 新建模块
- 添加依赖
- 更新依赖
- 添加新的 major 版本的依赖
- 更新依赖到新的 major 版本
- 移除未使用的依赖
新建模块
让我们来新建一个模块。
在 $GOPATH/src
目录外的任意目录中,新建一个空的文件夹。cd 进入该文件夹,新建一个源文件,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)
}
}
复制代码
现在,这个目录下包含一个 包(package)
,而不是一个 模块(module)
,因为没有 go.mod
文件。如果该目录为 /home/gopher/hello,运行 go test
,我们将看到:
$ go test
PASS
ok _/home/gopher/hello 0.020s
$
复制代码
最后一行列出了所有被测试的包。因为我们的目录在 $GOPATH
之外,也不属于任何模块,go 知道对于当前目录没有导入路径,所以 go 创建了一个基于当前文件路径的假模块:_/home/gopher/hello。
使用 go mod init
来把当前目录变成一个模块的根路径,然后再试一次 go test
:
$ 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
子目录,我们不需要在 world
中运行(也不想) go mod init
。这个包将自动成为模块 example.com/hello
的一部分,导入路径为 example.com/hello/world
。
添加依赖
Go 模块
的首要目标是提升使用别人写的代码(也是添加依赖)的体验。
让我们更新 hello.go
,导入 rsc.io/quote
依赖并使用它来实现 Hello:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
复制代码
现在,让我们再次运行测试:
$ 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 使用 go.mod
中的特定版本的依赖模块来解析包导入。当它遇到一个不属于 go.mod
中任何一个模块中的导入包时,它会自动寻找包含该缺失包的模块,并把这个模块的最新版本(最新版本
的定义为最新的一个被标记为稳定(stable)
的版本,如果没有则为最新的预览(prerelease)
版本,如果还是没有,则为最新的没有标记的版本)加入 go.mod
中。在我们的例子中,go test 解析新的 rsc.io/quote
导入为模块 rsc.io/quote v1.5.2
。它同时也下载了 rsc.io/quote
所需要的两个依赖:rsc.io/sampler
和 golang.org/x/test
。只有直接依赖会出现在 go.mod
文件中:
$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
$
复制代码
再次运行 go test 不会重复以上的工作,因为 go.mod
已经被更新,而且能下载的模块也缓存到了本地(在 $GOPATH/pgk/mod
中):
$ go test
PASS
ok example.com/hello 0.020s
$
复制代码
注意,虽然go 命令能轻松快速的添加一个新依赖,但这并非没有代价。你的模块现在依赖新的模块,需要注意很多特殊的点,例如:正确性、安全性以及合适的许可证,以上仅仅是几个例子。更多思考,见 Russ Cox 的这篇文章,Our Software Dependency Problem。
就如我们上面看到的,添加一个直接依赖往往会带来其他的非直接依赖。命令 go list -m
会列出当前模块和所有的依赖模块:
$ 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 list
输出中第一行永远是当前模块,也叫主模块
。接下来是按模块路径排序的依赖模块。
上面 golang.org/x/text version v0.0.0-20170915032832-14c0d48ead0c
是一个 pseudo-version
的例子,这是 go 对于没有标签的版本语法。
还有一点关于 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
都应该加入到版本控制中。
更新依赖
Go 模块中,版本号是语义化的。语义化版本号有三个部分:major、minor、patch。例如,版本号 v0.1.2
中,major 版本是 0,minor 版本是 1,patch 版本是 2。让我们试一试 minor 版本更新。下一节,进行 major 版本更新。
从 go list -m all
的输出中,我们可以看到,我们使用的是没有版本号的 golang.org/x/test
。让我们来升级到最新的版本,看看是否还能正常工作:
$ 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/test
包已经更新到最新的带版本号的版本(v0.3.0)。go.mod
中也更新到了 v0.3.0。indirect
注释表示它不是被本模块直接使用,而是被本模块依赖的模块使用。详情见 go modules
。
现在,让我们来试下更新 rsc.io/sampler
的 minor 版本。和上面一样,运行 go get
然后运行测试:
$ 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
$
复制代码
啊,测试失败。输出表明最新版的 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
来显示的指明版本,默认是 @latest
,也就是最新的版本。
添加一个新 major 版本依赖
我们来添加一个新函数:func Proverb
返回一个 Go 并发 proverb。这是通过调用 rsc.io/quote/v3
中的 quote
实现的。首先,我们在 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 := Prover(); got != want {
t.Error("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 模块的每一个不同的 major 的版本(v1, v2等等)都使用不同的模块路径:从 v2
开始,路径必须以 major 版本号结尾。例如, rsc.io/quote
的 v3 版本不同于 rsc.io/quote
,它们通过模块路径来区别。这种约定惯例称为 语义化导入版本(semantic import versioning),它给不兼容的包(不同的 major 版本号)不同的名字。与之对比,rsc.io/quote
的 v1.6.0 版应该向后兼容 v1.5.2 版,所以它能重用 rsc.io/quote
这个名字。(上节中,rsc.io/sampler
的 v1.99.99 版应该向后兼容 v1.3.0 版,但由于 bug 或者客户端错误的使用导致不兼容。)
go 构建的时候某个模块路径最多只能有一个版本,也就是每个 major 版本最多只能有一个:一个 rsc.io/quote
,一个 rsc.io/quote/v2
,一个 rsc.io/quote/v3
等等。这给模块作者一个模块可能重复使用的清晰规则:同时使用 rsc.io/quote
的 v1.5.2 和 1.6.0 是不允许的。同时,允许使用同一模块的不同 major 版本(因为它们有不同的模块路径),这给模块使用者升级到一个新的 major 版本的能力。例如,我们想使用 rsc.io/quote/v3
中的quote.Concurrency
,还我们还不准备对rsc.io/quote
v1.5.2 的使用进行升级合并。这种平滑升级合并是非常重要的,尤其是在大型软件或代码库中。
更新一个依赖到最新的 major 版本
现在让我们来完成从 src.io/quote
到 src.io/quote/v3
的升级转化。因为 major 版本号变了,那可能存在一些 API 被移除,或重命名,或有不兼容的变化。通过文档,我们发现 Hello
变成了 HelloV3
:
$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"
Package quote collects pithy sayings.
func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$
复制代码
我们将 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()
}
复制代码
现在,我们也不用重命名导入了,所以去掉重命名:
package hello
import "rsc.io/quote/v3"
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
复制代码
最后,运行测试看看一切都是否正确:
$ go test
PASS
ok example.com/hello 0.014s
复制代码
移除未使用的依赖
我们移除了所有关于 rsc.io/quote
的使用,但这个模块依然在 go list -m
和 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
$
复制代码
总结
Go 模块
是 Go 语言未来的依赖管理系统。模块功能在所有支持的 Go 版本中都是可用的(现在为,Go 1.11,Go 1.12)。
本文介绍了使用 Go 模块的工作流:
go mod init
创建一个新模块,并初始化go.mod
文件来描述该模块。go bulid
,go test
还有其他的包构建命令来添加一个新的依赖到go.mod
中。go list -m all
打印当前模块的依赖。go get
改变依赖的版本(或者添加新依赖)。go mod tidy
移除未使用的依赖。
我们鼓励你在个人本机开发中使用 Go 模块,并且把 go.mod
和 go.sum
加入到你的项目中。提供反馈和帮助 Go 中依赖管理完善,请发送 bug 报告 或 使用报告 给我们。
感谢你的反馈以及对模块功能提升的帮助。
译者总结
本文主要讲 Go 模块的基本用法,关于模块的更多概念和用法见 Github Page