介绍
本文是系列博客的第一部分。
- Part 1 —— 使用 Go Modules (本文)
- Part 2 —— 迁移到 Go Modules
- Part 3 —— 发布 Go Modules
- Part 4 —— Go Modules 的版本升级
- Part 5 —— Go Modules 兼容
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 添加一个依赖
- 升级依赖
- 升级 module 大版本后,添加一个新的依赖
- 升级 module 大版本后,更新一个已有的依赖
- 移除冗余的依赖项
创建一个新的 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:
- rsc.io/quote v1.5.2
- rsc.io/sampler v1.3.0
- golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
同时我们可以在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,变化有两个地方:
- import处移除了对rsc.io/quote的引用
- 方法Hello()中有原先的
return quote.Hello()
改为了return v3.HelloV3()
,包名和方法名都做了变更
至此,我们项目中对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 时涉及的几个命令:
-
go mod init
创建一个新的module,并初始化go.mod文件 -
go build, go test
将新增的依赖module信息加载到go.mod文件中 -
go list -m all
解析go.mod文件并打印当前module的依赖信息 -
go get
可以用来修改或者新增某个依赖module信息 -
go mod tidy
可以移除冗余的module信息
在实际开发过程中,我们提倡大家使用modules特性。