【译】Go 模块使用入门

栏目: Go · 发布时间: 5年前

内容简介:Go 1.11 和 1.12 版本初步支持了模块(modules)。它是 Go 中新的依赖管理系统,能更简单的显示、管理依赖的版本。本文介绍将一个Go 1.11 中,go 会在下面情况下使用
  • 原文标题:Using Go Modules
  • 原文作者:Tyler Bui-Palsulich and Eno Compton
  • 原文时间:2019-03-19

Go 1.11 和 1.12 版本初步支持了模块(modules)。它是 Go 中新的依赖管理系统,能更简单的显示、管理依赖的版本。本文介绍将 模块 使用入门所需要的一些基本操作。后续将有文章来涵盖跟多 模块 的使用。

一个 模块 就是一系列 Go packages 的集合,其存储在一个根目录中包含 go.mod 文件的文件夹中。 go.mod 文件定义了模块的 模块路径 ,这也是导入路径中的根路径,还定义了能成功构建本模块所需的依赖模块。每一项依赖表示为一个模块路径和一个特殊的语义化的版本号。

Go 1.11 中,go 会在下面情况下使用 模块 :当前目录或任何上级目录中有 go.mod 文件,且在 $GOPATH/src 目录之外(为了兼容,在 $GOPATH/src 目录中依然使用老的 GOPATH 模式,即使存在 go.mod ,详情见go 命令文档)。从 Go 1.13 开始, 模块模式 将成为默认的依赖管理系统。

本文介绍使用 模块 来开发 Go 项目时一些常用操作:

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

新建模块

让我们来新建一个模块。

$GOPATH/src 目录外的任意目录中,新建一个空的文件夹。cd 进入该文件夹,新建一个源文件, 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 知道对于当前目录没有导入路径,所以 go 创建了一个基于当前文件路径的假模块:_/home/gopher/hello。

使用 go mod init 来把当前目录变成一个模块的根路径,然后再试一次 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
$
复制代码

恭喜!你已经完成并测试了你的第一个 模块

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

现代编译原理

现代编译原理

Andrew W.Appel、Maia Ginsburg / 人民邮电 / 2005-9 / 59.00元

《现代编译原理:C语言描述(英文版)(本科)》全面讲述了现代编译器的各个组成部分,包括:词法分析、语法分析、抽象语法、语义检查、中间代码表示、指令选择、数据流分析、寄存器分配以及运行时系统等。与大多数编译原理的教材不同,《现代编译原理:C语言描述(英文版)(本科)》采用了函数语言和面向对象语言来描述代码生成和寄存器分配,对于编译器中各个模块之间的接口都给出了实际的 C 语言头文件。 全书分成两部分......一起来看看 《现代编译原理》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码