【译】Go 模块使用入门

  • 原文标题: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 项目时一些经常使用操做:缓存

  • 新建模块
  • 添加依赖
  • 更新依赖
  • 添加新的 major 版本的依赖
  • 更新依赖到新的 major 版本
  • 移除未使用的依赖

新建模块

让咱们来新建一个模块。安全

$GOPATH/src 目录外的任意目录中,新建一个空的文件夹。cd 进入该文件夹,新建一个源文件,hello.gobash

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 testui

$ 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/samplergolang.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.modgo.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 allgo.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,也就是最新的版本。

添加一个新 major 版本依赖

咱们来添加一个新函数: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/quotersc.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 的使用进行升级合并。这种平滑升级合并是很是重要的,尤为是在大型软件或代码库中。

更新一个依赖到最新的 major 版本

如今让咱们来完成从 src.io/quotesrc.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 -mgo.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,能轻松的发现缺什么包并须要加入到依赖中,却不能发现能安全移除的包。只有在检查一个模块中全部的包以后,才能肯定哪些依赖能安全移除。普通的构建命令不会加载全部的包,于是也不能安全的移除没有使用的依赖。

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 bulidgo test 还有其余的包构建命令来添加一个新的依赖到 go.mod 中。
  • go list -m all 打印当前模块的依赖。
  • go get 改变依赖的版本(或者添加新依赖)。
  • go mod tidy 移除未使用的依赖。

咱们鼓励你在我的本机开发中使用 Go 模块,而且把 go.modgo.sum 加入到你的项目中。提供反馈和帮助 Go 中依赖管理完善,请发送 bug 报告使用报告 给咱们。

感谢你的反馈以及对模块功能提高的帮助。

译者总结

本文主要讲 Go 模块的基本用法,关于模块的更多概念和用法见 Github Page

相关文章
相关标签/搜索