原文链接: https://blog.golang.org/using-go-modules

原文日期: 19 March 2019

Introduction

Go 1.11 和 1.12 初步支持了 modules,Go 新的版本管理系统使得版本信息更加明确,并且更容易管理。

这篇博文将会介绍介绍使用 modules 的基本操作,后续文章将介绍发布其他人使用的 modules

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s module path, which is also the import path used for the root directory, and its dependency requirements, which are the other modules needed for a successful build. Each dependency requirement is written as a module path and a specific semantic version.

在 Go 1.11,项目在 $GOPATH/src 外面,并且当前目录或者子目录存在 go.mod,go 命令行才会开启 modules(为了兼容性,在 $GOPATH/src 路径下,即使找到了 go.mod 也依然会使用 GOPATH 模式 ),Go 1.13 版本,modules 将会被默然开启在所有的环境中。

这篇文章将会介绍 Go 代码使用 modules 的一些列操作

  • 创建新的 modules
  • 添加依赖
  • 升级依赖
  • 添加主要版本的依赖
  • 升级主要版本的依赖
  • 移除没有使用的依赖

Creating a new modules

$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 所以不是一个 modules,

At this point, the directory contains a package, but not a module, because there is no go.mod file. If we were working in /home/gopher/hello and ran go test now, we’d see:

$ go test
PASS
ok      _/home/gopher/hello    0.020s
$

The last line summarizes the overall package test. Because we are working outside $GOPATH and also outside any module, the go command knows no import path for the current directory and makes up a fake one based on the directory name: _/home/gopher/hello.

让我们在之前的目录下执行 go mod init 并且再次尝试 go test

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok      example.com/hello    0.020s
$

恭喜你,你已经编写并测试了你的第一个 modules

go mod init 命令创建了 go.mod 文件

$ cat go.mod
module example.com/hello

go 1.12
$

go.mod 只会出现在 modules 的根目录,Packages in subdirectories have import paths consisting of the module path plus the path to the subdirectory,例如,如果我们创建了一个子目录 world,我们不需要并且不想在里面运行 go mod init,这个包将会自动被认为是 example.com/hello modules 的一部分,并且导入路径是 example.com/hello/world

Adding a dependency

modules 的主要动机是为了改善使用其他开发人员写的代码的体验,也就是添加依赖

让我们更新一下 hello.go ,导入 rsc.io/quote 并且使用它实现 Hello

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.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: 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 命令会自动查找 modules 包含的包并且添加到 go.mod 中,并使用 Latest 版本,(“Latest” is defined as the latest tagged stable (non-prerelease) version, or else the latest tagged prerelease version, or else the latest untagged version.)。在我们的例子中,go test 明确了 rsc.io/quote modules 的版本为 v1.5.2。也同时下载了两个 rsc.io/qutoe 使用的依赖,rsc.io/samplergolang.org/x/textgo.mod 只记录直接依赖关系

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

当再次使用 go test 时不会重复这个工作,应为 go.modup-to-date 并且下载了 modules 在本地缓存中($GOPATH/pkg/mod)

$ go test
PASS
ok      example.com/hello    0.020s
$

Note that while the go command makes adding a new dependency quick and easy, it is not without cost. Your module now literally depends on the new dependency in critical areas such as correctness, security, and proper licensing, just to name a few. For more considerations, see Russ Cox’s blog post, “Our Software Dependency Problem.”

正如上面我看到的,添加一个直接依赖经常会代理其他内置的依赖, go list -m all将会列出当前 modules 和所有依赖

$ 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的输出中,当前 modules 通常被认为是主要 modules,总是出现在第一行,followed by dependencies sorted by module path.

golang.org/x/text version v0.0.0-20170915032832-14c0d48ead0c 是一个 pseudo-version(伪版本) 的例子,它是特定无标记提交的 go 命令的版本语法。

go.mod 外,go 命令还维护了一个 go.sum 文件,其中包含特定 modules 版本内容的哈希

The go command uses the go.sum file to ensure that future downloads of these modules retrieve the same bits as the first download,确保 modules 的依赖不会意外更改,无论是恶意,意外或者其他原因, go.modgo.sum 都应该加到版本控制中。

Upgrading dependencies

对于 modules,应该使用 semantic 版本标识,semantic 版本包含是哪个部分,marjor,minir和patch,例如,对于 v0.1.2,主要版本是 0,次要版本是 1,并且补丁是 2。让我们看一下小版本的升级,在下一部分,我们会介绍主要版本的升级

通过 go list -m all的输出我们可以看到,我们使用没有标记的 golang/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
$

Woohoo,所有都通过了,让我再查看一下 go list -m allgo.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也升级到了指定版本 v0.3.0,indirext 说明这个依赖不是被当前 modules 直接需要,而且被其他 modules 需要。具体通过 go help modules 查看帮助。

现在让我们升级 rsc.io/sampler 的次要版本,通过同样的方法运行 go get 并且运行测试

$ 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
$

测试失败了,显示最新版本的 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
$

我们一直使用 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,在通常情况下,go get 可以带一个显式的版本,默认是 @latest,他解析为之前定义的最新版本。

Adding a dependency on a new major version

给我们的包添加一个新的功能,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
$

注意,我们的 modules 现在依赖 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
$

每一个不同的 modules 主要版本(v1,v2等)使用不同的 modules 路径:从 v2 开始,路径必须有主要版本结尾。在例子中 rsc.io/quote 的 v3 不再是 rsc.io/quote 取而代之的是 modules 的路径标识 rsc.io/quote/v3。这个约定被称为 semantic import versioning 并且提供了不兼容的包(不同的主要版本)不同名称,相比之下,rsc.io/quote v1.6.0 应该与 v1.5.2 兼容,因为它重用了 rsc.io/quote 名称。(再上一部分,v1.99.99 应该与 v1.3.0 兼容,但是由于 modules bug 或者其他步正确的客户端都有可能发生)

The go command allows a build to include at most one version of any particular module path, meaning at most one of each major version: one rsc.io/quote, one rsc.io/quote/v2, one rsc.io/quote/v3, and so on. 这就提供给作者单个 modules 可能重复的明确规则:不可能同时使用 rsc.io/quote v1.5.2 和 v1.6.0 进行构建。同时允许不同主要版本的 modules(因为他们有不同的路径)使模块使用者可以递增的方式升级到最新的主要版本。在我们的例子中,我们希望使用 rsc.io/quote/v3 v3.1.0 的 quote.Concurrency()但是尚未准备好迁移 rsc.io/quote v1.5.2 的使用。 The ability to migrate incrementally is especially important in a large program or codebase.

Upgrading a dependency to a new major version

让我们完成从 rsc.io/quote 转换到只使用 rsc.io/quote/v3,因为主要版本的改变,我们应该期望某些 APIs 在不兼容的道路上被移除,重命名或者其他方式的改变。阅读文档,我们发现 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
$

(这存在一个在输出的 已知 bug,the displayed import path has incorrectly dropped the /v3)

我们可以升级我们的 hello.go 中的 quote.Hello() 为新的 quoteV3.HelloV3()

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
    return quoteV3.HelloV3()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

此时我们不需要重命名导入,因为我们撤销它(注意 import 结尾的 /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

Removing unused dependencies

我们已经移除了 rsc.io/quote 的使用标识,但是通过 go list -m allgo.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 可以方便的判断哪些东西是缺失并且需要添加,但是不能判断哪些是被安全的移除。只有在检查模块中所有的包以及这些包的所有可能的构建标记组合之后才能删除依赖。普通的命令不会加载这些信息,所以不能安全的移除依赖。

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
$

Conclusion

Go module 是 Go 依赖管理的未来, 所有支持的 Go 版本(Go 1.11 和 Go 1.12)都支持 module 的所有功能。

这篇文章介绍了使用 Go module 的工作流

  • go mod init 创建一个新的 module,初始化它的描述文件 go.mod
  • go buildgo test 和其他一些构建命令根据需要为 go.mod 添加新的依赖项
  • go list -m all 打印当前 module 的所有依赖
  • go get 改变依赖所需要的版本(或者添加一个新的依赖)
  • go mod tidy 移除不使用的依赖

我们鼓励你在本地使用 modules 并且添加 go.modgo.sum 到你的项目中。 To provide feedback and help shape the future of dependency management in Go, please send us bug reports or experience reports.

Thanks for all your feedback and help improving modules.

By Tyler Bui-Palsulich and Eno Compton