34

Go 模块--开始使用 Go Modules

 4 years ago
source link: https://studygolang.com/articles/25846
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

6BbEJj6.png!web

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/[email protected]
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

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

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

6VNn6rV.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK