Go 的包管理一直被人诟病,有人提出过解决方法,好比 godep、govendor 等工具,但在 G1.11 版本中,Go 官方很霸道的提出了 Go Module 方案,虽然被人吐槽,但如今已经成为事实上的包管理方案。git
Go 官方也经过一系列的博客来介绍 Go Modules,这是系列的第一篇文章。github
这个系列的文章总共有 5 篇,这是第一篇:golang
使用 Go Modulesc#
迁移到 Go Modules缓存
发布 Go Modules安全
Go Modules:V2 及后续版本并发
保持 Modules 的兼容性ide
Go1.11 和 1.12 版本中初步支持了 Modules,Go 的新依赖管理系统使得依赖的版本信息更加清晰以及更容易管理。这篇文章将介绍 Go Modules 的基本使用。工具
一个 Module 是一系列 Go 的包组成的文件树,而且在根目录下有一个 go.mod 文件。go.mod 文件中定义了Module 的路径(module path),这也是根目录的包路径(import path),以及依赖需求,这是构建应用所须要的其余 Modules。每一个依赖都有一个 module path 和语义版本号。测试
从 Go1.11 开始,只要当前目录或者任何父目录中有 go.mod 文件,就可使用 module 相关的命令,但这个目录必需要在GOPATH/src以外。(在GOPATH/src 目录下,为了保持兼容性,即便 go.mod 文件存在,也只能运行老版本的命令。从命令文档中查看更多细节),从 Go1.13 开始,module 将做为默认的开发模式。
这篇文章介绍了使用 Go 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) } }复制代码
到这里,这个目录包含一个 package,但还不是 module,由于这里没有 go.mod 文件。若是咱们在 /home/gopher/hello
目录下,而后运行 go test
,就能够看到:
$ go test PASS ok _/home/gopher/hello 0.020s $复制代码
最后一行展现了测试的状况。由于当前不在 $GOPATH 下,也不在任何模块下,go 命令知道当前目录没有包路径(Import path),就基于当前的目录名称建立了一个假的包路径:_/home/gopher/hello。
接下来在当前的目录中使用 go mod init
来建立一个 module,而且再次运行 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 $复制代码
恭喜,你已经编写并测试了你的第一个 module。
go mod init 命令会建立一个 go.mod 文件:
$ cat go.mod module example.com/hello go 1.12 $复制代码
go.mod 文件只会出如今 module 的根目录。子目录中包的包路径具备由 module 路径和子目录路径组成。好比如今建立了一个子目录 world,不须要在目录中再次运行 go mod init。这个包会被自动设别为 example.com/hello
module 的一部分,包路径是 example.com/hello/world。
Go modules 被创造的最主要的动机是改善使用其余开发人员的代码的体验(即增长依赖)。
下面在 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 文件中。(Latest 表示最新标记的稳定(非预发布)版本,或者最新标记的预发布版本,或者最新的未标记版本。)在这个例子中,go test 会把 rcs.io/quote 解析为 rcs.io/quote 模块,版本为 v1.5.2。同时也会下载 rsc.io/quote 的两个依赖:rsc.io/sampler 和 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 命令能够快速、容易的添加依赖,但这是有代价的。你的模块功能的正确性、安全性、和许可证被你引入的那些新依赖决定(Ray注:意思是引入的这些依赖的安全性、程序是否被测试、是否引入了侵权的代码,这些都无法保证),而这只是其中的一小部分问题。更深刻的思考,请查看 Russ Cox 的博客,咱们的软件依赖问题。
在前面能够看到,直接引入的依赖一般也会引入一些间接的依赖。 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 list 的输出中,当前模块,也被称之为主模块会出如今第一行,下面跟着依赖的模块路径。
golang.org/x/text 的版本号 v0.0.0-20170915032832-14c0d48ead0c 叫作 Pseudo-versions,这是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 的模块中,版本号使用语义版原本表示。一个语义版本有三个部分:主版本号、次版本号、补丁版本号。好比 v0.1.2,主版本是 0, 次版本是 1,补丁版本号 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 文件中也被更新到了 0.3.0 版本。 indirect
表示这个依赖不是直接被模块使用,只是间接的被其余的模块依赖。经过 go help modules
能够查看更多的细节。
如今,让我试着用一样的方法来更新 rsc.io/sample 的次版本号,先执行 go get 命令,而后执行 go test 命令:
$ 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 这个版本号。一般来讲,go get 命令均可以接收一个特定的版本号,默认是 @latest,表示以前定义的最新版本。
让咱们在包中添加一个新的方法, Proverb
方法返回一个 Go 的并发谚语,经过 rsc.io/quote/v3 中的quote.Concurrency
方法来提供。首先在 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 $复制代码
如今这个模块中 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 等等)都使用不一样的模块路径,从 v2 开始,路径必须以主版本号结尾。在这个例子中,rsc.io/quote 的 v3 版本的路径再也不是 rsc.io/quote,而是 rsc.io/quote/v3。这种习惯被称之为语义导入版本,会给不兼容的包(拥有不一样的主版本号)不一样的名称。相反 rsc.io/quote 的 v1.6.0 必须向后兼容 v1.5.2,因此会重用 rsc.io/quote 这个路径名称。(在以前的版本中,rsc.io/sampler v1.99.99 应该向后兼容 rsc.io/sample v1.3.0,可是由于 bug 或者不正确的客户端存在,模块的这些行为都是有可能发生的。)
go 命令在构建中只容许任何特定的模块存在至多一个主版本,意味着只能每一个模块的主版本只能出现一次:一个 rsc.io/quote,一个 rsc.io/qutoe/v2,一个 rsc.io/quote/v3,以此类推。这为模块做者提供了关于单个模块路径可能重复的明确规则:rsc.io/quote v1.5.2 和 rsc.io/quote v1.6.0 不能同时出如今同一次构建中。同时,容许有不一样主版本的模块出现类同一个构建中(由于拥有不一样的路径),这也给模块的消费者能够有增量升级主版本的能力。在这个例子中,咱们想要调用rsc/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 中,咱们能够把 quote.Hello() 使用 V3.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.mod 文件中,经过 go list -m all 命令看到:
$ 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 modules 是 Go 依赖管理的将来。模块功能能够在全部支持的 Go 版本中使用(包括 Go1.11 和 Go1.12)。
这篇文章中介绍了 Go modules 的这些功能:
go mod init 建立一个新的模块,并初始化描述这个模块的 go.mod 文件
go build,go test 和其余包内构建命令添加须要的新依赖到 go.mod 文件中
go list -m all 打印当前模块全部的依赖
go get 改变当前依赖的新版本(或者添加一个新的依赖)
go mod tidy 移除无用的依赖
咱们鼓励你在本地的开发中在项目中添加 go.mod 和 go.sum,开始使用模块功能。请向咱们发送bug 报告和体验报告,帮助咱们改善 Go 的依赖管理功能。
感谢您全部的反馈和帮助改进模块的建议。