简介
Go 1.11 和 1.12 都对模块有了初步的支持,使得对依赖的管理更加详细和容易。这个文章对模块的使用做一个基本的介绍。
模块是对Go包一个集合,以一个文件树的形式存储在根目录的go.mod
文件中。go.mod
文件定义了模块的路径,还有相关的依赖项。每一个依赖项都会被认为是一个模块路径和指定的版本规则,其实就是依赖项也是一个模块。
在Go 1.11的时候,Go命令行就有了对模块的支持,当当前的目录或者父目录有go.mod
文件的时候,并且可以在GOPATH
以外的地方使用,为了兼容性,即使在GOPATH
中发现了go.mod
文件,也会使用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)
}
}
复制代码
这是,当前目录中包含了一个包,并不是一个模块,因为这里还没有go.mod
文件,如果我们在当前目录下执行go test
,可以看到:
D:\Code\go>go test
PASS
ok _/D_/Code/go 0.327s
D:\Code\go>
复制代码
最后一行总结了整体包测试。 因为我们在$GOPATH
之外以及任何模块之外工作,所以go命令不知道当前目录的导入路径,并根据目录名称构成假路径:_/D_/Code/go
。
现在,我们在当前目录下执行go mod init
,然后再进行go test
:
D:\Code\go>go mod init example.com/hello
go: creating new go.mod: module example.com/hello
D:\Code\go>go test
PASS
ok example.com/hello 0.393s
D:\Code\go>
复制代码
恭喜!你已经写了一个模块,并且通过了测试
go mod init
命令生成一个go.mod
文件:
PS D:\Code\go> cat .\go.mod module example.com/hello
go 1.12
PS D:\Code\go>
复制代码
go.mod
文件只会出现在模块的根目录,子目录中的包具有导入路径,包括模块路径和子目录的路径。例如,我们创建一个子目录world
,我们可以不在再次执行go mod init
,包会自动加载并且可以被识别出是example.com/hello
的一部分,导入路径就是example.com/hello/world
。
添加依赖
Go模块的主要作用是提升可以使用其他开发人员提供的代码。
更新hello.go
,导入rsc.io/quote
包:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
复制代码
再一次执行go test
D:\Code\go>go test
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
--- FAIL: TestHello (0.00s)
hello_test.go:10: Hello() = "Hello, world.", want "Hello, world"
FAIL
exit status 1
FAIL example.com/hello 0.431s
D:\Code\go>
复制代码
go命令会解析go.mod
文件中指定的依赖和版本。当遇见一个在go.mod
中没有提供的包时,go命令会自动查找这个包并且添加到go.mod
文件中,使用最新的版本(最新的版本指的时被打标签的稳定版,或者最新的预发布版本,或者最新的版本),在我们这个例子中,go test
解析了一个新的包rsc.io/quote
,版本为v1.5.2。还下载了两个包rsc.io/quote
使用的两个依赖项,就是rsc.io/sampler
和golang.org/x/test
。go.mod
文件指记录直接的依赖关系:
D:\Code\go>type go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
D:\Code\go>
复制代码
再一次执行go test
的时候,就不会重复上一次的任务了,因为go.mod
现在已经更新了,并且依赖已经被缓存到了本地($GOPATH/pkg/mod)
正如我们看到的,添加一个依赖经常会带来其他的依赖,go list -m
命令可以列出当前模块的所有依赖
yankeweideMacBook-Pro:hello yankewei$ go list -m all
hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
yankeweideMacBook-Pro:hello yankewei$
复制代码
在输出的内容中,当前的模块名总是会出现在第一行,紧跟着按依赖的模块路径排序。
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
是一个伪版本号,这个go系统对于一个没有打标的commit的标识。
除了go.mod之外,go命令还维护一个名为go.sum的文件,其中包含特定模块版本内容的预期加密哈希:
yankeweideMacBook-Pro:hello yankewei$ 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=
yankeweideMacBook-Pro:hello yankewei$
复制代码
go使用go.sum文件来确保未来下载这些模块的时候保证和第一次下载的一样,也确保你项目的依赖不会改变,所以go.mod和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/test
包已经被更新到了最新的版本(v0.3.0)。indirect
注释表示依赖没有被这个模块直接使用,仅仅被其他的模块间接使用,可以通过go help modules
查看相信信息。
现在让我们尝试更新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.014s
$
复制代码
Shit!测试失败了,最新的版本和我们要使用的不兼容,先看一下这个模块的可用版本:
$ 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
,也就是最新的版本
添加一个新的主版本
我们添加一个新的函数func Proverb
, 函数会调用rsc.io/quote/v3
的模块,在这个模块中可以调用quote.Concurrency
,返回一个字符串,首次我们先来更新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
每个不同的主版本(v1,v2等等)的模块使用的是不同的路径,这样就可以选择性的使用,并且在版本迁移的时候也可以逐步的进行。
更新依赖为新的主版本
现在我们想要把rsc.io/quote
更新到rsc.io/quote/v3
。因为主版本号已经变了,我们可能会意识到某些api被移除,被重命名或者调用方法发生了改变,所以首先我们需要先看一下文档:
$ 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 改为了 HelloV3,那我们就可以把使用的 Hello 改为 HelloV3,并且文件中没有对旧版本的依赖了,可以把导入重命名去掉:
package hello
import (
"rsc.io/quote/v3"
)
func Hello() string {
return quote.HelloV3()
}
func Proverb() string {
return quote.Concurrency()
}
复制代码
可以自行测试,就不演示了。
删除未使用的依赖
在Go语言中删除未使用的依赖相当的简单,只需要执行go mod tidy
。
有疑问加站长微信联系(非本文作者)