【译】使用 Go Modules

2021-01-21  本文已影响0人  王小奕

原文链接

介绍

本文是系列博客的第一部分。

Go 1.11 版本初步添加了对 modules 特性的支持,作为 go 语言新的依赖管理系统,可以帮助我们更明确、容易的管理依赖包的版本信息。在这篇文章中,我们将会介绍入门 modules 特性所需掌握的一些基本操作。

module 是一系列包的集合,以文件树的形式记录在go.mod文件中,该文件位于项目根路径。go.mod文件主要定义了module的路径(也就是使用该module的import路径值),以及该module本身对其他module的依赖信息。其中每一个依赖项module都包含该module的导入路径及专门的版本号,版本号要符合MAJOR.MINOR.PATCH格式。

从 Go 1.11 开始,如果当前工作路径或其父路径下存在go.mod文件,且不在$GOPATH/src目录内的话,go 命令会默认使能modules特性,反之,如果在$GOPATH/src目录内的话,为了兼容起见,go命令还会以传统的GOPATH模式运作,即便有go.mod文件存在。但从 Go 1.13 开始,module模式将会成为默认模式。即不管当前路径是否位于$GOPATH/src路径下,只要含go.mod文件则默认开启module模式,除非你修改了环境变量GO111MODULE值,将其置为了off 。

本文将会从以下几个步骤来介绍使用 modules 特性开发过程中所涉及的几个通用操作:

创建一个新的 module

现在我们开始创建一个新的 module 。
$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)
    }
}

此时,这个目录下包含一个包——hello,但因为没有go.mod文件,所以还算不上是一个module,这时候我们运行go test命令可以看到:

C:\Users\Lenovo\IdeaProjects\studyGoModules>go test
PASS
ok      _/C_/Users/Lenovo/IdeaProjects/studyGoModules   0.240s

C:\Users\Lenovo\IdeaProjects\studyGoModules>

很奇怪,我们既没有在$GOPATH/src目录下工作,同时当前目录也没有go.mod文件,按理说hello_test.go会报找不到Hello()方法的错误,但为啥跑过了呢?原来是go命令找不到任何路径,于是基于其当前工作路径名编造了一个假的的导入path:_/C_/Users/Lenovo/IdeaProjects/studyGoModules,通过这种方法,他找到了hello.go文件中定义的Hello()方法,使得测试跑过了。

接下来,我们通过命令go mod init来创建一个根路径为当前路径的module并且再次执行go test查看效果:

C:\Users\Lenovo\IdeaProjects\studyGoModules>go mod init gitee.com/atix/hello
go: creating new go.mod: module gitee.com/atix/hello

C:\Users\Lenovo\IdeaProjects\studyGoModules>go test
PASS
ok      gitee.com/atix/hello    0.244s

C:\Users\Lenovo\IdeaProjects\studyGoModules>

可以看到我们创建了一个名为```gitee.com/atix/hello``的module,并且测试了这个module的可用性。同时还在当前路径创建了一个go.mod文件:

module gitee.com/atix/hello

go 1.14

go.mod文件只会出现在一个module的根路径下。我们可以在某个module目录中新建子目录并创建新的package,只需在引用该package时,import路径包含上module的import路径即可。如:我们上面创建了一个新的module:gitee.com/atix/hello,我们在根目录下新建文件夹:subHello,并新建文件subHello.go:

package subHello

import "fmt"

func SubHello() string {
    return "SubHello, world!"
}

这样,当我们需要在别的地方使用SubHello()方法时(当然,首先我们得发布hello module),import路径要加上hello module的路径,即:

import "gitee.com/atix/hello/subHello"

为该 module 添加一个依赖

Go Modules 的主要目的是更好的使用其他开发者开发的代码。
接下来我们尝试在hello module中使用其他开发者发布的module,首先我们改写一下hello.go:

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

然后我们再次运行go test命令看下效果(因本人网络问题访问https://proxy.golang.org/rsc.io/quote/@v/list 异常,故此处贴原文执行效果):

$ 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      gitee.com/atix/hello    0.023s
$

这里我找到了rsc.io/quote的go.mod文件:

module "rsc.io/quote"

require "rsc.io/sampler" v1.3.0

以及rsc.io/sampler的go.mod文件:

module "rsc.io/sampler"

require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c

可以看到,从hello到quote,再到sampler,依次依赖了下面3个module:

同时我们可以在go test的执行日志中看到这3个module的find->downloading->extract操作,即:go 命令会分析go.mod文件,并递归式的处理require模块所声明的依赖项module,处理内容包括下载及解压,解压后的依赖项module内容会缓存在$GOPATH/pkg/mod路径中。

go test执行的效果还包括:将解析后的依赖项module信息添加到本module的go.mod文件中,注意,默认只添加直接依赖的module。关于依赖项module的版本号,默认使用其最新版本。体现为:

$ cat go.mod
module gitee.com/atix/hello

go 1.14

require rsc.io/quote v1.5.2
$

值得注意的是,虽然 go 命令使得添加依赖变得超级方便,但这是有代价的。你会发现自己的模块在某些关键领域,比如说正确性、安全性以及许可性等方面会相当依赖新添加的依赖项modules。这方面想了解更多的话,可以参阅博客Our Software Dependency Problem

正如上面我们看到的那样,添加一个直接依赖的同时也会带来一些间接依赖。执行命令go list -m all可以查看当前module的所有依赖项信息:

$ go list -m all
gitee.com/atix/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$

注意此命令会递归式分析go.mod文件,如果你跟我一样因网络原因执行go test失败导致go.mod文件没更新的话,此命令只会显示本module(别名:主 module )信息,即上面的第一行:gitee.com/atix/hello 。
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c :此依赖项版本号被称为"Pseudo-versions"式版本号,代表该module代码的某次提交版本,该提交尚未打tag,由3部分组成,代码提交前最近的一次版本号,代码提交时间的UTC格式,以及代码提交号的hash前缀。更多详情戳此

除了go.mod文件外,go命令还维护了一个名为go.sum的文件,该文件主要包含各依赖项module版本内容的加密哈希值。如果未依赖其他module则不会生成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.sum文件的帮助,go命令可以确保将来下载这些module的代码时能够获取跟第一次下载时相同的内容,以保证你的项目依赖的这些module不会发生意想不到的改变,不管是恶意的,偶然的还是意外的。故,请将go.mod、go.sum文件都纳入版本管理。

升级依赖

使用 Go Modules 功能过程中,版本号要符合一定的语法格式。一个合法格式包括3部分:主版本、次版本、补丁版本。举个例子,拿v0.1.2来说,主版本号是0,次版本号是1,补丁版本是2。接下来,我们一起来对gitee.com/atix/hello进行一次次版本号升级。

通过分析命令go list -m all的输出结果,我们可以看到golang.org/x/text模块的版本是一个未打tag的版本号,接下来我们把 golang.org/x/text升级到最新的一个tag版本并测试一下项目是否依然有效:

$ 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 get,完整的命令,在module名后面需要加上@version字段来指明要获取该module哪个版本,默认情况下会获取其最新版本

很棒,项目运行正常,接下来让我再看一眼项目的依赖情况是否发生了变化:

$ 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 gitee.com/atix/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
)
$

注释// indirect表示这一行的依赖并不是本module直接依赖使用的,而是其他module依赖使用的。想了解更多的话可以执行命令go help modules来查看细节。

可以看到golang.org/x/text已经升级到了最新的版本,同时go.mod文件也更新了,增加了golang.org/x/text模块的描述信息,前面我们提到过,默认情况下执行go test时,go.mod文件只会加载直接依赖的module信息,为啥这次把golang.org/x/text这个间接依赖的module信息加上了呢,这是因为我们在本地执行了go get golang.org/x/text命令后,本地开发调试用到该module的代码时都会用本地的最新版本golang.org/x/text代码,而不是之前那个未打tag的版本,这种情况下,如果我们发布了自己的module,别人引用时,才能正常使用。

需要注意的是,通过本地go get方式直接覆盖原依赖module版本时会出现不兼容问题,换句话说,本来以来的那个module版本是v1.1.0,但你通过go get方式获取的是v1.2.0,且该module的v1.2.0未兼容v1.1.0,那么使用时就会出错。因此,如果想要本地升级某个依赖module的版本,最好在get的时候指定某个具体的版本,如:go get rsc.io/sampler@v1.3.1

命令go list -m -versions ${module_name}可以列出${module_name}所有可用版本

添加一个新的依赖,该依赖是某个已有依赖module的大版本升级中引入的特性

截止目前为止,我们的gitee.com/atix/hello模块直接依赖的module只有一个rsc.io/quote v1.5.2,接下来我们将要引入一个新的module:rsc.io/quote/v3@latest,并在我们代码中调用其Concurrency()方法,修改hello.go文件如下:

package hello

import (
    "rsc.io/quote"
    v3 "rsc.io/quote/v3"
)

func Hello() string {
    return quote.Hello()
}

func V3Hello() string {
    return v3.Concurrency()
}

同时在测试代码中添加对新方法V3Hello()的测试方法:

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)
    }
}

func TestV3Hello(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := V3Hello(); got != want {
        t.Errorf("V3Hello() = %s, want %s", got, want)
    }
}

接下来执行go test查看效果:

$ 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      gitee.com/atix/hello    0.024s
$

此时,如果我们查看go.mod文件时,会发现require块同时包含rsc.io/quote v1.5.2和rsc.io/quote/v3 v3.1.0两行,那么这个要怎么理解呢?

回顾我们本段标题,什么叫升级大版本?对于每一个module来说,主版本升级意味着不同的module path,从v2开始,每升级一个大版本,该module的import路径都要加一层。拿本例中的rsc.io/quote来说,第一次我们import时写的是import rsc.io/quote,那么当go命令在解析依赖关系时,就回去拉rsc.io/quote第一个大版本的最新版,即v1.3.2,后来我们新import了import rsc.io/quote/v3,那么当go命令在解析依赖关系时,就回去拉rsc.io/quote第三个大版本的最新版,即v3.1.0。

这是go语言对module版本的语法定义,默认情况下,同一个大版本内的小版本之间,应该是向下兼容的。这样如果我们之前依赖了某个module的低版本,当后期需要升级其次版本号以使用某个新增的方法或者特性时,就不需要修改import的path,只需要执行go get XXX@YYY来获取其新的版本即可。

对于同一个module path,在同一次build中,go命令最多只允许include一次。通过变更module path的方式来升级大版本号可以允许我们在一次build同时include该module的多个版本,因为他们的module path不同。这在某些时候尤其有用,想象这样一种场景,作为使用方,我们在项目中依赖了某个module A,某天A升级了大版本,添加了某个有用的特性,但新版本是否兼容了旧版本尚未可知,此时我们既想用新特性,又没有那么多时间去验证之前使用部分的兼容性,怎么办,没关系,只要A遵循了大版本升级修改module path的原则,那么我们可以在项目中同时使用A新旧两个版本,只需要在import时对新版本加个别名即可,如前面我们的做法:import v3 rsc.io/quote/v3 就是给quote的大版本3起了个别名v3。

将已有的某个依赖module升级到其新的大版本

上一节中,我们同时引入了rsc.io/quote的v1和v3两个版本,虽然在当时是方便了,但现在我们想要简化依赖树,仅使用rsc.io/quote的最新版本。这时候我们要考虑至少以下几个方面,大版本升级后:

此时最直接的方法通读rsc.io/quote/v3的更新文档来了解,查阅某个module的文档可以通过执行go doc命令实现:

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

可以看到rsc.io/quote/v3中已不再有Hello()方法,新的方法名为HelloV3(),此时我们修改我们项目中对Hello()方法的使用,如下:

package hello

import v3 "rsc.io/quote/v3"

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

func V3Hello() string {
    return v3.Concurrency()
}

注意对比两个版本的hello.go,变化有两个地方:

至此,我们项目中对rsc.io/quote的依赖就变成单一的rsc.io/quote/v3了,就不再需要在import对其进行别名处理了:

package hello

import "rsc.io/quote/v3"

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

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

这里有个小插曲,楼主这边是用的是IDEA,该工具在判断import包的使用情况时比较死板,只认最后一层路径,他发现你没有使用v3,就会把import那行给删掉,坑爹

移除未使用的依赖

虽然上面我们删掉了rsc.io/quote的导入,但当我们执行go list -m all或者查看go.mod文件时,发现其仍然存在:

$ go list -m all
gitee.com/atix/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 gitee.com/atix/hello

go 1.14

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命令知会检查是否有包丢失了,或者需要添加进来,但却不会移除冗余的包。想要移除冗余的module信息可以执行命令go mod tidy来实现:

$ go mod tidy
$ go list -m all
gitee.com/atix/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 gitee.com/atix/hello

go 1.14

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      gitee.com/atix/hello    0.020s
$

总结

Go Modules 是未来 Go 语言的依赖管理体系,且此特性在 Go 1.11 之后均支持。

围绕go.mod文件,本文介绍了使用 Go Modules 时涉及的几个命令:

在实际开发过程中,我们提倡大家使用modules特性。

上一篇下一篇

猜你喜欢

热点阅读