Go1.11和1.12包含了对模块的初步支持,这是新版本Go中的依赖项管理系统,它使依赖项版本信息更易于管理。本文介绍了如何使用模块所需的基本操作。

 

模块是存储在带有Go的文件目录树中的Go包的集合,通常在其根目录下通过go.mod文件保存这些依赖信息。go.mod文件定义了模块路径(也就是根目录使用的导入路径)和依赖项需求(这是成功构建所需要的第三方模块)。每个依赖项需求都被编写为模块路径和特定的语义版本。

 

从Go1.11开始,Go命令允许在当前目录或任何父目录有go.mod文件时使用模块功能,前提是目录在$GOPATH/src之外。在$GOPATH/src中,为了兼容性,go命令仍然在旧的GOPATH模式下运行,即使已经定义了go.mod。更详细的信息,可以参阅go命令文档(go command documentation)。从Go1.13开始,模块模式将是所有开发环境的的默认模式。

 

这篇文章将介绍在使用模块开发Go代码时常用的一系列操作:

  • 创建新模块

  • 添加依赖项

  • 更新依赖项

  • 添加新的主版本作为依赖项

  • 将依赖项升级到新的主版本

  • 删除未使用的依赖关系

 

创建新模块

首先创建一个新模块。在$GOPATH/src之外的某个地方创建一个新的空目录,在cmd命令窗口中通过cd进入该目录,然后创建一个新的源文件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)
    }
}

此时目录下包含一个package,但还未创建模块及对应的go.mod文件。如果此时在/home/gopher/hello中运行go test,会有如下输出:

$ 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 test
PASS
ok      example.com/hello    0.020s

恭喜,第一个模块已经编写并测试通过。gomod 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模块的主要目标是提升依赖项的管理,特别是使用其他开发人员编写的代码的体验。下一步更新一下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命令自动查找包含该包的模块并将其添加到go.mod并使用该依赖包的最新版本。(“最新”定义为最新标记的稳定版本,或最新标记的预发布版本,或最新未标记的版本。)在示例中,gotest解析了新导入的rsc.io/quote(v1.5.2)包。它还下载了rsc使用的两个依赖项rsc.io/sample和golang.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 test
PASS
ok      example.com/hello    0.020s

 

请注意,虽然go命令使添加新依赖项变得简单,但也是有代价的。可以参考Russ Cox的博客文章“OurSoftware Dependency Problem”。正如在上面看到的,添加一个直接依赖项通常也会带来其他间接依赖项。命令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列表输出中,当前模块(也称为主模块)总是第一行,然后是按模块路径对依赖项排序。golang.org/x/text version v0.0.0-20170915032832-14c0d48ead0c是伪版本(pseudo-version)的一个例子,伪版本是go命令针对特定未标记提交的版本语法。除了go.mod,go命令维护一个名为go.sum的文件,其中包含特定模块版本内容的加密散列(cryptographic hashes):

$ 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.mod和go.sum应该提交给版本控制系统。

 

更新依赖项

Go模块中版本由语义版本标签引用(semantic version tags),语义版本由三部分组成:主要(major)、次要(minor)和补丁(patch)。例如,对于v0.1.2,主版本是0,次要版本是1,补丁版本是2。接下来尝试对一个引用包的主要版本(major version)进行升级更新。

 

从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

测试结果显示一切正常。再看看golist -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文件也已更新为v0.3.0版本。注释表明依赖项不是由这个模块直接使用的,而是由其他模块依赖项间接使用的。现在尝试升级rsc.io /sample的minor版本,同样运行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)b
    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/sample不兼容。看看该模块的可用版本:

$ 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.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(即最新版本)。

 

添加新的主版本作为依赖项

先在代码中添加一个新函数:func Proverb() ,通过调用quote.Concurrency()并返回Go concurrencyproverb对象。这个方法定义在模块rsc.io/quote/v3中:

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:

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0

 

Go模块的每个不同的主版本(v1、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构建。同时,允许模块的不同主版本(因为它们有不同的路径)使模块使用者能够增量地升级到新主版本。在本例中,我们想使用quote.Concurrency(来自rsc/quote/v3 v3.1.0),但是还没有完全准备好迁移原先使用rsc.io/quote v1.5.2的代码。增量迁移的能力在大型程序或代码库中尤其重要。

 

将依赖项升级到新的主版本

让我们升级到新的主版本,只使用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

(注:输出中有一个已知的bug;显示的导入路径删除了/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()

}

 

现在,不需要重命名导入了:

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一样,可以很容易地判断什么时候缺少了什么东西,什么时候需要添加,但是什么时候可以安全地删除什么东西?只有在检查模块中的所有包以及这些包的所有可能的构建标记组合之后,才能删除依赖项。普通构建命令不加载此信息,因此不能安全地删除依赖项。

 

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中依赖管理的未来。模块功能现在可以在所有受支持的Go版本中使用(即Go 1.11和Go1.12中)。这篇文章介绍了如何使用Go模块的操作指引:

  • go mod init创建一个新模块,初始化go.mod文件。

  • go build、go test和其他构建命令会在需要时添加新依赖项到go.mod文件中。

  • go list -m all输出当前模块的依赖项。

  • go get更改依赖项的所需版本(或添加新的依赖项)。

  • go mod tidy删除未使用的依赖项。

 

原文作者:Tyler Bui-Palsulich/ Eno Compton  译者:江玮