5

1_Z2TSiC3zcKDjctaK9za-WA.png
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/samplergolang.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.modgo.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.0indirect注释指明依赖项不被当前模块直接使用,而是由其依赖的模块所使用的。

现在,让我们尝试升级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/quotersc.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 init 创建一个新模块,初始化描述它的go.mod文件。
  • go buil,go test和其他程序包构建命令根据需要向go.mod添加新的依赖项。
  • go list -m all打印当前模块的依赖关系。
  • go get更改所需依赖的版本(或添加新的依赖)。
  • go mod tidy删除未使用的依赖项。

参考文章:https://blog.golang.org/using...

现在越来越多的项目都开始用Go Modules来管理依赖包,我也是刚开始尝试将现有项目迁移到用 Go Modules管理的模式,在实践中发现还有很多要学习的地方,后期会分享更多这方面的学习文章和总结。


Kevin
5k 声望1.5k 粉丝