- 原文标题:Using Go Modules
- 原文做者:Tyler Bui-Palsulich and Eno Compton
- 原文时间:2019-03-19
Go 1.11 和 1.12 版本初步支持了 模块(modules)。它是 Go 中新的依赖管理系统,能更简单的显示、管理依赖的版本。本文介绍将模块
使用入门所须要的一些基本操做。后续将有文章来涵盖跟多模块
的使用。git
一个模块
就是一系列 Go packages
的集合,其存储在一个根目录中包含 go.mod
文件的文件夹中。go.mod
文件定义了模块的 模块路径
,这也是导入路径中的根路径,还定义了能成功构建本模块所需的依赖模块。每一项依赖表示为一个模块路径和一个特殊的语义化的版本号。github
Go 1.11 中,go 会在下面状况下使用模块
:当前目录或任何上级目录中有 go.mod
文件,且在 $GOPATH/src
目录以外(为了兼容,在 $GOPATH/src
目录中依然使用老的 GOPATH 模式,即便存在 go.mod
,详情见 go 命令文档)。从 Go 1.13 开始,模块模式
将成为默认的依赖管理系统。golang
本文介绍使用模块
来开发 Go 项目时一些经常使用操做:缓存
让咱们来新建一个模块。安全
在 $GOPATH/src
目录外的任意目录中,新建一个空的文件夹。cd 进入该文件夹,新建一个源文件,hello.go
:bash
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)
,而不是一个 模块(module)
,由于没有 go.mod
文件。若是该目录为 /home/gopher/hello,运行 go test
,咱们将看到:函数
$ go test
PASS
ok _/home/gopher/hello 0.020s
$
复制代码
最后一行列出了全部被测试的包。由于咱们的目录在 $GOPATH
以外,也不属于任何模块,go 知道对于当前目录没有导入路径,因此 go 建立了一个基于当前文件路径的假模块:_/home/gopher/hello。测试
使用 go mod init
来把当前目录变成一个模块的根路径,而后再试一次 go test
:ui
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok example.com/hello 0.020s
$
复制代码
恭喜!你已经完成并测试了你的第一个模块
。
go mod init
命令将如下内容写入 go.mod
文件中:
$ cat go.mod
module example.com/hello
go 1.12
$
复制代码
go.mod
文件仅出如今模块的根路径中。子目录下的包的导入路径为模块路径加上子目录路径。例如,咱们新建一个 world
子目录,咱们不须要在 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
中任何一个模块中的导入包时,它会自动寻找包含该缺失包的模块,并把这个模块的最新版本(最新版本
的定义为最新的一个被标记为稳定(stable)
的版本,若是没有则为最新的预览(prerelease)
版本,若是仍是没有,则为最新的没有标记的版本)加入 go.mod
中。在咱们的例子中,go test 解析新的 rsc.io/quote
导入为模块 rsc.io/quote v1.5.2
。它同时也下载了 rsc.io/quote
所须要的两个依赖:rsc.io/sampler
和 golang.org/x/test
。只有直接依赖会出如今 go.mod
文件中:
$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2
$
复制代码
再次运行 go test 不会重复以上的工做,由于 go.mod
已经被更新,并且能下载的模块也缓存到了本地(在 $GOPATH/pgk/mod
中):
$ go test
PASS
ok example.com/hello 0.020s
$
复制代码
注意,虽然go 命令能轻松快速的添加一个新依赖,但这并不是没有代价。你的模块如今依赖新的模块,须要注意不少特殊的点,例如:正确性、安全性以及合适的许可证,以上仅仅是几个例子。更多思考,见 Russ Cox 的这篇文章,Our Software Dependency Problem。
就如咱们上面看到的,添加一个直接依赖每每会带来其余的非直接依赖。命令 go list -m
会列出当前模块和全部的依赖模块:
$ 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
输出中第一行永远是当前模块,也叫主模块
。接下来是按模块路径排序的依赖模块。
上面 golang.org/x/text version v0.0.0-20170915032832-14c0d48ead0c
是一个 pseudo-version
的例子,这是 go 对于没有标签的版本语法。
还有一点关于 go.mod
,就是 go 会维护一个名为 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 使用这个 go.sum
来确保未来下载的依赖版本和第一次下载的是如出一辙的,以确保你项目中依赖的模块没有改变,不管是恶意的或意外的仍是其余缘由致使的改变。go.mod
和 go.sum
都应该加入到版本控制中。
Go 模块中,版本号是语义化的。语义化版本号有三个部分:major、minor、patch。例如,版本号 v0.1.2
中,major 版本是 0,minor 版本是 1,patch 版本是 2。让咱们试一试 minor 版本更新。下一节,进行 major 版本更新。
从 go list -m all
的输出中,咱们能够看到,咱们使用的是没有版本号的 golang.org/x/test
。让咱们来升级到最新的版本,看看是否还能正常工做:
$ 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/test
包已经更新到最新的带版本号的版本(v0.3.0)。go.mod
中也更新到了 v0.3.0。indirect
注释表示它不是被本模块直接使用,而是被本模块依赖的模块使用。详情见 go modules
。
如今,让咱们来试下更新 rsc.io/sampler
的 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)
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
来显示的指明版本,默认是 @latest
,也就是最新的版本。
咱们来添加一个新函数:func Proverb
返回一个 Go 并发 proverb。这是经过调用 rsc.io/quote/v3
中的 quote
实现的。首先,咱们在 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 := Prover(); got != want {
t.Error("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 模块的每个不一样的 major 的版本(v1, v2等等)都使用不一样的模块路径:从 v2
开始,路径必须以 major 版本号结尾。例如, rsc.io/quote
的 v3 版本不一样于 rsc.io/quote
,它们经过模块路径来区别。这种约定惯例称为 语义化导入版本(semantic import versioning),它给不兼容的包(不一样的 major 版本号)不一样的名字。与之对比,rsc.io/quote
的 v1.6.0 版应该向后兼容 v1.5.2 版,因此它能重用 rsc.io/quote
这个名字。(上节中,rsc.io/sampler
的 v1.99.99 版应该向后兼容 v1.3.0 版,但因为 bug 或者客户端错误的使用致使不兼容。)
go 构建的时候某个模块路径最多只能有一个版本,也就是每一个 major 版本最多只能有一个:一个 rsc.io/quote
,一个 rsc.io/quote/v2
,一个 rsc.io/quote/v3
等等。这给模块做者一个模块可能重复使用的清晰规则:同时使用 rsc.io/quote
的 v1.5.2 和 1.6.0 是不容许的。同时,容许使用同一模块的不一样 major 版本(由于它们有不一样的模块路径),这给模块使用者升级到一个新的 major 版本的能力。例如,咱们想使用 rsc.io/quote/v3
中的quote.Concurrency
,还咱们还不许备对rsc.io/quote
v1.5.2 的使用进行升级合并。这种平滑升级合并是很是重要的,尤为是在大型软件或代码库中。
如今让咱们来完成从 src.io/quote
到 src.io/quote/v3
的升级转化。由于 major 版本号变了,那可能存在一些 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
中的 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
和 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,Go 1.12)。
本文介绍了使用 Go 模块的工做流:
go mod init
建立一个新模块,并初始化 go.mod
文件来描述该模块。go bulid
,go test
还有其余的包构建命令来添加一个新的依赖到 go.mod
中。go list -m all
打印当前模块的依赖。go get
改变依赖的版本(或者添加新依赖)。go mod tidy
移除未使用的依赖。咱们鼓励你在我的本机开发中使用 Go 模块,而且把 go.mod
和 go.sum
加入到你的项目中。提供反馈和帮助 Go 中依赖管理完善,请发送 bug 报告 或 使用报告 给咱们。
感谢你的反馈以及对模块功能提高的帮助。
本文主要讲 Go 模块的基本用法,关于模块的更多概念和用法见 Github Page