翻译自
https://blog.golang.org/using-go-modules
Tyler Bui-Palsulich and Eno Compton
19 March 2019
译注:2019-10,练习可能需要科学上网
Introduction
这是系列里的第一篇:
- Part 1 - Using Go Modules (此篇)
- Part 2 - Migrating To Go Modules
- Part 3 - Publishing Go Modules
Go 1.11和1.12 包含模块(module)的初步支持。本篇介绍如何开始使用模块,后续篇会讲解如何发布模块。
一个模块就是一组Go包(package),这些包存在于一棵文件树,在根目录有个go.mod文件。Go.mod定义了模块的module path (也是根目录的import path)、以及依赖 (就是为了成功编译所需的其他模块)。每个依赖写成一个module path + 特定的语义版本 (semantic version)。
Go 1.11时,如果目录在 $GOPATH/src 外面,则只要当前目录或其任意上级目录包含go.mod 文件,go命令就会启用模块模式 ( $GOPATH/src里面,即使存在go.mod,仍然使用旧的GOPATH模式)。Go 1.13开始模块模式是默认的。
本篇讲述Go模块编程的一系列操作:
- 创建新模块
- 添加依赖
- 升级依赖
- 添加新的主要版本 (major version) 依赖
- 升级依赖到主要版本
- 删除多余的依赖
Creating a new 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/src外面、也在任何模块外面,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 只出现在模块的根目录,其子目录的import path = module path + 子目录路径。例如子目录 world (不需也不应该在world 运行 go mod init ) 会自动被识别为模块 example.com/hello 的包,import path = example.com/hello/world 。
Adding a dependency
模块的主要动机是方便使用其他人开发的代码。
更新 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 来解析导入 (import)。当发现一个不属于go.mod 里任一模块的导入包时,go命令自动查找包含该包的模块并添加到go.mod - 查找时使用 最近版本 (latest version):最近tagged的稳定版 或prerelease版、或untagged版。上例里导入 rsc.io/quote 被解析为模块 rsc.io/quote v1.5.2。命令也下载了 rsc.io/quote 的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/pkg/mod ):
$ go test
PASS
ok example.com/hello 0.020s
$
注意go命令使得添加依赖很容易,但也是有代价的,你需要考虑自己模块所依赖代码的correctness、security、licensing等。更多考虑参考Russ Cox的博客“Our Software Dependency Problem” (https://research.swtch.com/deps)。
如上添加一个直接依赖常常导致其他非直接依赖。命令 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
$
当前模块 - 称为主模块 main module - 总是输出在第一行。其余行按module path 排序。
golang.org/x/text 的版本 v0.0.0-20170915032832-14c0d48ead0c 是 pseudo-version (https://golang.org/cmd/go/#hdr-Pseudo_versions) 的例子,也是go命令表达特定untagged commit的版本语法。
除了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 都应该提交到版本库。
Upgrading dependencies
Go模块的版本使用semantic version标签。Semantic version包含三部分:major、minor、patch。例如版本v0.1.2:major是0,minor是1,patch是2。下面试验minor版本的升级,下一节试验major版本的升级。
从 go list -m all 可看到我们在使用一个untagged 的 golang.org/x/text 版本。下面升级其到最近的tagged版本并测试:
$ 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 包已经被升级到最近的tagged版本 (v0.3.0)。go.mod 文件同样也被更新了。注释 indirect 表示此依赖不是直接被此模块使用,而是间接地被其他模块使用。细节可参考 go help modules。
现在试着升级rsc.io/sampler 的minor版本:
$ 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 的最近版本与此项目不兼容。让我们列出该模块所有可用的tagged版本:
$ 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 指定版本。通常每个传递给go get 的参数都可以明确带上版本,默认是 @latest 即之前解释的 最近版本。
Adding a dependency on a new major version
现在让我们添加一个新方法:func Proverb 来返回一个Go格言。此方法是通过调用模块rsc.io/quote/v3 的方法quote.Concurrency :
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模块的每个不同major版本 (v1, v2, …) 使用不同的module path:从v2 开始,必须以major版本结尾。如例子里v3 版本的rsc.io/quote 不再是rsc.io/quote,而是变成rsc.io/quote/v3。这个约定称为 semantic import versioning (https://research.swtch.com/vgo-import),它给予不兼容的包不同的major版本 (也就是不同的名字)。相对照地,v1.6.0 版本的rsc.io/quote 应该向后与v1.5.2 版本兼容,所以它沿用名字rsc.io/quote (在之前小节里rsc.io/sampler v1.99.99 本应该与v1.3.0 兼容,但模块bug和不正确的使用假定都可能发生)。
go模块对于一个module path编译仅允许包含一个版本,意味着每个major版本最多一个:一个rsc.io/quote、一个rsc.io/quote/v2、一个rsc.io/quote/v3,…。这给了模块作者关于是否重复一个module path的清晰规则:不可能同时使用rsc.io/quote v1.5.2 和rsc.io/quote v1.6.0 来编译一个程序。同时,允许使用一个模块的不同major版本 (因为path不同) 使得模块的使用者可以逐步升级代码。在例子里我们想使用rsc.io/quote/v3 v3.1.0 的quote.Concurrency,但还没有准备好升级调用rsc.io/quote v1.5.2 的代码。允许代码增量迁移对于大的代码库尤其重要。
Upgrading a dependency to a new major version
让我们彻底完成从rsc.io/quote 到rsc.io/quote/v3 的迁移。由于major版本的变化,可以预期有些API 被移除、改名、或以不兼容的方式修改。阅读doc,可以看到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
$
(Go 1.12有个bug:显示的import path缺少结尾的 /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()
}
现在,不需要重命名导入了。于是:
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
Removing unused dependencies
我们已经移除了所有调用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,可以很容易发现什么缺少需要添加,但难以发现什么可以安全地删除。删除一个依赖必须检查模块里所有的包、以及这些包所有可能的build tag组合。一个普通的编译命令没有加载这些信息,所以不能安全地删除依赖。
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
$
Conclusion
Go模块是Go依赖管理的未来。模块功能现在在所有支持的Go版本里都可用 (Go 1.11、1.12、1.13)。
本篇介绍使用Go模块的工作流程如下:
- go mod init 创建一个模块,初始化go.mod 文件
- go build, go test 和其他包编译命令添加依赖到go.mod 文件里
- go list -m all 打印当前模块的所有依赖
- go get 改变一个依赖的版本 (或添加一个依赖)
- go mod tidy 移除不用的依赖
我们鼓励你开始使用模块,添加go.mod, go.sum 到项目。为提供反馈和帮助塑造Go依赖管理的未来,请发送错误报告 (https://golang.org/issue/new) 和体验报告 (https://golang.org/wiki/ExperienceReports)。
谢谢反馈和帮助改进模块功能。