Using Go Modules

本文详细介绍如何使用Go模块进行依赖管理,包括创建模块、添加依赖、升级依赖、删除多余依赖等操作,以及如何处理不同主要版本的依赖。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

翻译自

https://blog.golang.org/using-go-modules
Tyler Bui-Palsulich and Eno Compton
19 March 2019

译注:2019-10,练习可能需要科学上网

Introduction

这是系列里的第一篇:

  • Part 1 - Using Go Modules (此篇)
  • Part 2 - Migrating To Go Modules
  • Part 3 - Publishing Go Modules

Go 1.11和1.12 包含模块(module)的初步支持。本篇介绍如何开始使用模块,后续篇会讲解如何发布模块。

一个模块就是一组Go包(package),这些包存在于一棵文件树,在根目录有个go.mod文件。Go.mod定义了模块的module path (也是根目录的import path)、以及依赖 (就是为了成功编译所需的其他模块)。每个依赖写成一个module path + 特定的语义版本 (semantic version)。

Go 1.11时,如果目录在 $GOPATH/src 外面,则只要当前目录或其任意上级目录包含go.mod 文件,go命令就会启用模块模式 ( $GOPATH/src里面,即使存在go.mod,仍然使用旧的GOPATH模式)。Go 1.13开始模块模式是默认的。

本篇讲述Go模块编程的一系列操作:

  • 创建新模块
  • 添加依赖
  • 升级依赖
  • 添加新的主要版本 (major version) 依赖
  • 升级依赖到主要版本
  • 删除多余的依赖

Creating a new 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)
    }
}

此时目录下只包含一个包而非模块,因为没有go.mod文件。如果当前目录是 /home/gopher/hello ,运行 go test 将输出:

$ go test
PASS
ok      _/home/gopher/hello    0.020s
$

因为目前在$GOPATH/src外面、也在任何模块外面,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 只出现在模块的根目录,其子目录的import path = module path + 子目录路径。例如子目录 world (不需也不应该在world 运行 go mod init ) 会自动被识别为模块 example.com/hello 的包,import path = example.com/hello/world

Adding a dependency

模块的主要动机是方便使用其他人开发的代码。

更新 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 来解析导入 (import)。当发现一个不属于go.mod 里任一模块的导入包时,go命令自动查找包含该包的模块并添加到go.mod - 查找时使用 最近版本 (latest version):最近tagged的稳定版 或prerelease版、或untagged版。上例里导入 rsc.io/quote 被解析为模块 rsc.io/quote v1.5.2。命令也下载了 rsc.io/quote 的2个依赖:rsc.io/samplergolang.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命令使得添加依赖很容易,但也是有代价的,你需要考虑自己模块所依赖代码的correctness、security、licensing等。更多考虑参考Russ Cox的博客“Our Software Dependency Problem” (https://research.swtch.com/deps)。

如上添加一个直接依赖常常导致其他非直接依赖。命令 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
$

当前模块 - 称为主模块 main module - 总是输出在第一行。其余行按module path 排序。

golang.org/x/text 的版本 v0.0.0-20170915032832-14c0d48ead0c 是 pseudo-version (https://golang.org/cmd/go/#hdr-Pseudo_versions) 的例子,也是go命令表达特定untagged commit的版本语法。

除了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 都应该提交到版本库。

Upgrading dependencies

Go模块的版本使用semantic version标签。Semantic version包含三部分:major、minor、patch。例如版本v0.1.2:major是0,minor是1,patch是2。下面试验minor版本的升级,下一节试验major版本的升级。

go list -m all 可看到我们在使用一个untagged 的 golang.org/x/text 版本。下面升级其到最近的tagged版本并测试:

$ 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/text 包已经被升级到最近的tagged版本 (v0.3.0)。go.mod 文件同样也被更新了。注释 indirect 表示此依赖不是直接被此模块使用,而是间接地被其他模块使用。细节可参考 go help modules

现在试着升级rsc.io/sampler 的minor版本:

$ 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 的最近版本与此项目不兼容。让我们列出该模块所有可用的tagged版本:

$ 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 即之前解释的 最近版本。

Adding a dependency on a new major version

现在让我们添加一个新方法:func Proverb 来返回一个Go格言。此方法是通过调用模块rsc.io/quote/v3 的方法quote.Concurrency

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/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, …) 使用不同的module path:从v2 开始,必须以major版本结尾。如例子里v3 版本的rsc.io/quote 不再是rsc.io/quote,而是变成rsc.io/quote/v3。这个约定称为 semantic import versioning (https://research.swtch.com/vgo-import),它给予不兼容的包不同的major版本 (也就是不同的名字)。相对照地,v1.6.0 版本的rsc.io/quote 应该向后与v1.5.2 版本兼容,所以它沿用名字rsc.io/quote (在之前小节里rsc.io/sampler v1.99.99 本应该与v1.3.0 兼容,但模块bug和不正确的使用假定都可能发生)。

go模块对于一个module path编译仅允许包含一个版本,意味着每个major版本最多一个:一个rsc.io/quote、一个rsc.io/quote/v2、一个rsc.io/quote/v3,…。这给了模块作者关于是否重复一个module path的清晰规则:不可能同时使用rsc.io/quote v1.5.2rsc.io/quote v1.6.0 来编译一个程序。同时,允许使用一个模块的不同major版本 (因为path不同) 使得模块的使用者可以逐步升级代码。在例子里我们想使用rsc.io/quote/v3 v3.1.0quote.Concurrency,但还没有准备好升级调用rsc.io/quote v1.5.2 的代码。允许代码增量迁移对于大的代码库尤其重要。

Upgrading a dependency to a new major version

让我们彻底完成从rsc.io/quotersc.io/quote/v3 的迁移。由于major版本的变化,可以预期有些API 被移除、改名、或以不兼容的方式修改。阅读doc,可以看到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
$

(Go 1.12有个bug:显示的import path缺少结尾的 /v3)

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

Removing unused dependencies

我们已经移除了所有调用rsc.io/quote 的代码,但是在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/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,可以很容易发现什么缺少需要添加,但难以发现什么可以安全地删除。删除一个依赖必须检查模块里所有的包、以及这些包所有可能的build tag组合。一个普通的编译命令没有加载这些信息,所以不能安全地删除依赖。

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
$

Conclusion

Go模块是Go依赖管理的未来。模块功能现在在所有支持的Go版本里都可用 (Go 1.11、1.12、1.13)。

本篇介绍使用Go模块的工作流程如下:

  • go mod init 创建一个模块,初始化go.mod 文件
  • go build, go test 和其他包编译命令添加依赖到go.mod 文件里
  • go list -m all 打印当前模块的所有依赖
  • go get 改变一个依赖的版本 (或添加一个依赖)
  • go mod tidy 移除不用的依赖

我们鼓励你开始使用模块,添加go.mod, go.sum 到项目。为提供反馈和帮助塑造Go依赖管理的未来,请发送错误报告 (https://golang.org/issue/new) 和体验报告 (https://golang.org/wiki/ExperienceReports)。

谢谢反馈和帮助改进模块功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值