Go的1.11和1.12版本包括对模块--新的Go依赖管理系统的初步支持,使依赖版本信息变得明确且更易于管理。这篇博客文章介绍了开始使用模块所需的基本操作。
模块是存储在根目录有一个 go.mod
文件的文件树中的 Go 包(package)的集合。go.mod
文件定义了模块的module path(也是模块根目录的导入路径)以及模块依赖的其他模块的要求,满足了依赖要求模块才能被成功构建起来。每个依赖模块的要求被写为一个模块路径和相应的模块版本。
下面展示了一个简单的go.mod
文件
module example.com/hello go 1.12 require rsc.io/quote v1.5.2
从Go 1.11开始,当当前目录或任何父目录有go.mod
时,只要该目录位于$GOPATH/src
之外,go命令就可以使用模块。 (在$ GOPATH/src
内部,出于兼容性考虑,即使找到了go.mod
,go命令仍然在旧的GOPATH模式下运行。)从Go 1.13开始,模块模式将是所有开发的默认模式。
本文介绍了使用模块开发Go代码时出现的一系列常见操作:
在$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) } }
假设我们新建的目录为/home/gopher/hello
,此时该目录包含一个包,而不是模块,因为目录中没有go.mod
文件。使用 go 命令运行测试会看到:
$ go test PASS ok _/home/gopher/hello 0.020s $
输出的最后一行汇总了整个包的测试信息。因为我们工作在$GOPATH
和任意模块之外,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 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
。
现在再运行go test
其运行结果如下:
$ go test PASS ok example.com/hello 0.020s $
现在输出中的导入路径变成了example.com/hello
,不知不觉中就编写并测试了我们的第一个go模块。
Go模块的主要动机是改善管理使用其他开发者编写的代码(代码依赖)的体验。 让我们更新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
命令使用在go.mod
中列出的指定的依赖模块版本来解析导入,当遇到未由go.mod
中的任何模块提供的包的导入时,go
命令将自动查找包含该软件包的模块,使用其最新的稳定版本,并将其添加到go.mod中。 在我们的示例中,go test
将新的导入rsc.io/quote
解析为rsc.io/quote v1.5.2
模块,它还下载了rsc.io/quote
使用的两个依赖项,即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 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 list
的输出中,当前模块也被称为主模块,总是会出现在第一行,后面跟随的是根据模块路径排序后展示的依赖项:
除了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.sum
并不是类似package-lock.json
的包管理器锁文件,它是一个构建状态跟踪文件。它会记录当前模块所有的直接和间接依赖,以及这些依赖的校验和,从而提供一个可以100%复现的构建过程并对构建对象提供安全性的保证。所以应该将go.mod
和go.sum
都添加到版本控制中。go.sum
同时还会保留过去使用的包的版本信息,以便日后可能的版本回退,这一点也与普通的锁文件不同。所以go.sum并不是包管理器的锁文件。
对于Go模块,使用语义版本标记引用模块版本。语义版本包括三个部分:主要,次要和补丁。例如,对于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
文件中golang.org/x/text
也已更新为指定的v0.3.0
。indirect
注释指明依赖项不被当前模块直接使用,而是由其依赖的模块所使用的。
现在,让我们尝试升级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 $
我们将 rsc.io/sampler
升级到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。通常,传递给get的每个参数都可以采用显式形式。默认值为@latest,它将解析为先前定义的最新版本。
让我们在包中添加一个新函数:函数Proverb
通过调用quote.Concurrency
返回Go并发谚语(就是Pike说在某年 Go 开发大会上说的金句:"Concurrency is not parallelism"),这是由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 $
可以看到 go 命令下载安装了rsc.io/quote/v3
模块,现在我们的模块同时依赖了 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模块的每个不同的主要版本(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
。
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的使用。在大型程序或代码库中,增量迁移的能力尤其重要。
让我们完成从使用rsc.io/quote
两个版本的包到仅使用rsc.io/quote/v3
的转换。由于版本的重大更改,我们应该期望某些API可能已以不兼容的方式被删除,重命名或以其他方式更改。阅读文档,我们可以看到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 $
我们可以把hello.go
中对qoute.Hello()
的调用更新为使用quoteV3.HelloV3()
,现在已经不需要对 v3 版本的导入路径重命名了所以我们撤销包的重命名(注意默认包名不会包含版本后缀)。
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 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
命令不会加载此信息,因此它不能安全地删除依赖项。
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依赖管理的未来。从 Go1.11都提供模块功能。 这篇文章介绍了使用Go模块的这些工作流程:
go.mod
添加新的依赖项。参考文章:https://blog.golang.org/using...
现在越来越多的项目都开始用Go Modules来管理依赖包,我也是刚开始尝试将现有项目迁移到用 Go Modules管理的模式,在实践中发现还有很多要学习的地方,后期会分享更多这方面的学习文章和总结。