Go 1.11和1.12包含了基础的modules,Go的最新的依赖管理系统,使得依赖版本的信息更加显而易见,管理更加方便。本文介绍了如何使用Go modules的基本操作。
一个module是多个Go packages的集合,在module的根目录下会生成一个文件go.mod。go.mod文件定义了这个module的依赖目录,每一个依赖的表现形式就是在go.mod 中记录的一行路径和特殊的版本描述。
从Go 1.11中,当当前目录或任何父目录具有go.mod时,只要该目录位于GOPATH/src目录内,为了兼容性,Go仍然会使用旧的GOPATH模式,即使目录中有go.mod文件。)从Go1.13开始,Go 的module模式作为默认的功能。
如何使用Go modules?本文从以下几节讲解:
- 创建一个新的module
- 添加依赖
- 升级依赖
- 添加一个依赖的新的升级版本
- 升级一个依赖到新的版本
- 移除不用的依赖
创建一个新的module
首先,在$GOPATH/src之外的一个地方创建一个新的空目录,在本次实验中,创建一个example空目录,位于/root/projects/hello
下,在hello
目录下创建一个hello.go
源文件。
package hello
func Hello() string {
return "Hello, world."
}
编写test文件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)
}
}
此时hello还并不是一个go module,因为里面没有包含go.mod文件,此时我们运行go test
:
# go test
PASS
ok _/root/projects/hello 0.014s
最后一行总结了整体package的测试情况。因为我们在$GOPATH之外以及任何模块之外运行go,所以go命令不知道当前目录的导入路径,而是根据目录名_/root/projects/hello组成一个伪目录。
接下来我们使用go mod:
# go mod init example.com/hello
go: creating new go.mod: module example.com/hello
# go test
PASS
ok example.com/hello 0.010s
此时,我们成功创建了第一个go module。并且go mod init
命令创建了go.mod
文件在hello目录下:
# cat go.mod
module example.com/hello
go 1.13
go.mod文件仅出现在模块的根目录中。子目录中的程序包具有由模块路径以及子目录路径组成的导入路径。例如,如果我们创建了一个子目录world
,则无需(也不希望)在那里运行go mod init。该软件包将自动被识别为example.com/hello模块的一部分,导入路径为example.com/hello/world。
添加依赖
Go module设计的初衷是为了改善使用其他开发人员编写的代码(即添加对代码的依赖)的体验。
此时我们在hello.go
中导入一个第三方的依赖包rsc.io/quote
并且使用它:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
运行go test
:
[root@struma1 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: 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
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok example.com/hello 0.039s
[root@struma1 hello]#
通过运行结果,我们可以看出: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文件中:
[root@struma1 hello]# cat go.mod
module example.com/hello
go 1.13
require rsc.io/quote v1.5.2
[root@struma1 hello]#
如果我们再次运行go test
,将不会再次去下载依赖,因为此时 go.mod
已经是最新的并且下载的依赖包已经缓存在本地目录$GOPATH/pkg/mod
中:
[root@struma1 hello]# go test
PASS
ok example.com/hello 0.014s
[root@struma1 hello]#
[root@struma1 hello]# ls $GOPATH/pkg/mod/cache/download
golang.org rsc.io sumdb
从上文可知,当我们引入一个依赖包时,往往这个包还会间接依赖其它的包,go list -m all
会列出所有的依赖包:
[root@struma1 hello]# 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.mod
之外,还会创建出一个go.sum
文件,此文件中包含了使用依赖包的具体的包的版本的哈希值:
[root@struma1 hello]# cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
go命令使用go.sum文件来确保这些模块,在将来下载与第一次下载使用相同的包,以确保项目所依赖的模块不会由于恶意,意外或其他原因而意外更改。应该将go.mod和go.sum都检查到版本控制中。
升级依赖
对于Go模块,使用语义版本标记引用版本。语义版本包括三个部分:主要,次要和补丁。例如,对于v0.1.2,主要版本为0,次要版本为1,补丁版本为2。让我们逐步进行几个次要版本升级。
从go list -m all的输出中,我们可以看到我们正在使用未标记版本的golang.org/x/text。让我们升级到最新的标记版本,并测试一切是否正常:
[root@struma1 hello]# go get golang.org/x/text
go: finding golang.org/x/text v0.3.2
go: downloading golang.org/x/text v0.3.2
go: extracting golang.org/x/text v0.3.2
[root@struma1 hello]#
[root@struma1 hello]# go test
PASS
ok example.com/hello 0.017s
[root@struma1 hello]#
# go list -m all
go: finding golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
# cat go.mod
module example.com/hello
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote v1.5.2
)
由此可见 test
包已经升级到0.3.2的最新版本,go.mod
文件中也对应记录了test
包的最新版本号,注释中的indirect
表示,此模块未直接使用依赖关系,而其他模块依赖关系仅间接使用该依赖关系。接下来让我们来升级 rsc.io/sampler
包的次要版本。
# 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.016s
竟然测试失败了,看来是我们升级的新版本不兼容老版本,我们可以查看一下这个包的可用的版本:
# 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
既然这个版本不能用,我们可以尝试使用其他版本试试:
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.025s
此处要注意的是,@v1.3.1
是go get命令的参数,通常,传递给get的每个参数都可以采用显式形式。默认值为@latest
,它将解析为先前定义的最新版本。
添加一个新的主要版本
我们通过引入一个新的包rsc.io/quote/v3
来使用包中的一个新的函数Proverb
:
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
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 list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
每个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。 (在上一节中,rsc.io/sampler v1.99.99应该已经与rsc.io/sampler v1.3.0向后兼容。)
升级依赖到新的主要版本
我们用新版本rsc.io/quote/v3
。因为是大版本的改变,所以我们首先确认一个api有没有什么改动:
# 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
更新代码:
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.018s
清理无用的依赖包
此时我们已经不再使用依赖包rsc.io/quote
,但是当我们查看依赖时,还是会列出此依赖:
# go list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
为什么?因为构建单个软件包(例如使用go build或go test)可以轻松判断何时缺少某些内容并需要添加,但无法确定何时可以安全删除。仅在检查模块中的所有软件包以及这些软件包的所有可能的构建标记组合之后,才能删除依赖项。普通的build命令不会加载此信息,因此它不能安全地删除依赖项。
# go mod tidy
# go list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
# cat go.mod
module example.com/hello
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
[root@struma1 hello]# go test
PASS
ok example.com/hello 0.016s
不用的依赖包成功删除了。
本文参考链接:Go官方文档
有疑问加站长微信联系(非本文作者)