Golang依赖包管理知多少

前言

本文内容结合自己的理解,对Golang官方博客针对Golang新版本modules包管理系统的文章翻译。

使用Go modules

Go从1.11和1.12已经初步支持modules,新版Go包依赖管理系统通过显性的版本依赖使得更加易于维护。这篇文章主要介绍了在开始使用Golang modules时的一些基本操作。

一个module是许多Go Pakcage的集合,在它的根目录有一个go.mod文件。go.mod文件中定义这个module的module路径,也定义了需要导入的module路径、外部依赖,这些都是modules成功构建所必须的。每一个外部依赖都有一个module路径和版本号组成。

从Go 1.11开始,如果当前目录或者它的父级目录中有go.mod文件并且此目录不在$GOPATH/src下面,go命令就会开启对modules的支持(如果在$GOPATH/src目录下,为了保持兼容即便存在go.mod文件go命令还会使用原有的GOPATH模式)。从Go 1.13开始module模式就会默认启用了。

下面就开始介绍在使用module模式开发Go代码过程中的几个步骤:

创建一个新的module

创建一个不在$GOPATH/src的空目录,cd到这个目录,然后创建一个golang源文件hello.go:

package hello

func Hello() string {
 return "Hello,World."
}

再写一个测试用例:

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了,但是它还不是一个module因为它缺少 go.mod 文件。下面在这个目录下运行 go test 执行单元测试,我们会看到:

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

恭喜你,你编写并测试通过了第一个module。然后再当前目录下使用 go mod init命令生成一个 go.mod 文件:

$ cat go.mod
module example.com/hello

go 1.12
$

go.mod文件只会出现在module的根目录下,但是在子目录的package在import时则必须将module路径和子目录一并写上,举个例子,如果我们创建一个world子目录,那我们没必要在这个目录下再执行go mod init命令,package会自动识别它是example.com/hello 这个module的一部分,自动补充上import路径为 example.com/hello/world。

增加一个依赖包

Go modules机制的主要目的就是为了方便使用或者依赖三方开发者的代码。

那我们现在更新一下hello.go文件,import一个rsc.io.quote库并且使用它:

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命令就会import go.mod文件中列出来的module依赖版本。当它遇到一个import引入了但在go.mod的module中没有引用时go命令会自动查找这个package并把它的最新版本(通常是最新标记的稳定版本,或者是最新标记的预发布版本,抑或是未尿急版本)加入到go.mod文件中。在我们的例子中,go test解决rsc.io.quote时使用v1.5.2版本,同时它也会下载rsc.io.quote的两个依赖,分别是rsc.io/samplergolang.org/x/text,但是只有直接依赖会被记录在go.mod文件中,此时的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的博文,。

正如我们看到的那样,增加一个新的直接依赖通常会引入一些其他的依赖,go list -m all命令会列出当前module的所有依赖包:

$ 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的输出中,当前module作为主module以module path的样式出现在第一行,紧跟着其他依赖。

golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c是一个伪版本号的例子,它是go命令拉取的一个没有tag标记的commitId的特殊版本标识。

除了go.mod文件以外,go命令还维护着一个go.sum文件,它包含着某个module版本的Hash值:

$ 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文件来维持某个module在后面下载时和第一次下载时是相同的文件,确保工程的module依赖不会因为恶意、偶然等因素而出现异常,所以go.mod、go.sum这两个文件应该被版本管理工具维护起来。

升级依赖包的小版本

在go module中,版本是文本语义的标识,它包含三部分:主版本、次版本、Patch版本,例如v0.1.2,主版本是0,次版本是1,patch版本是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文件也升级到了v0.3.0。indirect注释标记了这个依赖包不是被当前module直接依赖,只是被其他module依赖。

现在我们尝试升级一下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
$

OMG,测试用例未通过,最新版本的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,可以固定下载某个版本,如果没有则下载@latest

增加一个大版本的依赖包

我们在hello包中增加一个Proverb的函数,通过quote.Concurrency返回Go的并发原语,Concurrencyrsc.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
$

但是要注意的是我们的module现在同时依赖着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 module中module的不同主版本使用不同的module path:从v2开始path必须以主版本为结尾。例如rsc.io/quote的v3版本module path就是rsc.io/quote/v3。这叫做文本标识导入版本,它赋予不兼容的依赖包不同的名字。当然,v1.6.0版本的rsc.io/quote应该向后兼容v1.5.2版本,所以它可以复用rsc.io/quote(就像前面那个例子,rsc.io/sampler v1.9.99应当向后兼容rsc.io/sampler` v1.3.0,当然不正确的执行或者问题肯定也同时出现)。

go命令构建时允许特定module path最多包含一个版本,也就只允许每个主版本最多一个,比如rsc.io/quote,rsc.io/quote/v2,rsc.io/quote/v3等等。这种方式对module的作者来说是清晰的,同一个module path不可能出现多个版本,对module的使用者来说也能够渐进式的升级主版本,比如我们准备使用rsc.io/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/v3"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$

那我们更新一下hello.go文件,使用quoteV3.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
$

我们重新运行测试用例:

$ 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 buildgo test可以很容易的告诉我们何时缺失了什么依赖包、需要增加什么依赖包,但是不知道什么时间来哪些依赖包可以安全地移除。移除一个依赖包只能在检查完module中所有依赖包以及所有可能的构建标签以后来执行,但普通的构建命令并不能提供这些信息,所以它也不能安全的移除依赖。

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 modules是golang未来的依赖管理工具,在Go1.11、1.12以及之后的版本已经提供支持。本文介绍的go modules工作流如下:

  • go mod init命令创建一个新的module,初始化go.mod文件;
  • go buildgo test等其他构建命令可以增加新的依赖到go.mod文件
  • go list -m all命令输出当前module的所有依赖包信息
  • go get改变依赖包的版本(或者增加一个新的依赖包)
  • go mod tidy移除未使用的依赖包

我们强烈建议你在本地开发环境通过增加go.mod、go.sum开始使用modules。

原文链接

迁移Go Modules

发布Go Modules

Go Modules版本迭代