Go的包管理工具(三):Go Modules

23,027 阅读6分钟

在前面的文章,我们先是介绍了Go 的几种包管理方式,然后具体介绍了一种包管理的工具: glide。随着 Go 1.11 的发布,官方的包管理工具 Go Modules 变得流行起来。在发布不久的 Go 1.12 版本中,增强了对 Go Modules 的支持。本文将会介绍如何在项目中安装和使用 Go Modules

安装和激活 Modules 的支持

前置条件

如本文开头所说,从 Go 1.11 版本才支持 Go Modules。所以,默认 Go 的版本为 >= 1.11。

$ go version
go version go1.12 darwin/amd64

笔者安装了最新的 1.12 版本。

激活使用

安装后,我们可以通过以下两种方式之一激活模块支持:

  • $GOPATH/src 之外的目录中调用 go 命令,且当前目录或其任何父目录中使用有效的 go.mod 文件,并且环境变量 GO111MODULE 未设置(或显式设置为auto)。
  • 在环境变量集上设置 GO111MODULE = on 后,调用go命令。

如何定义模块

为当前的项目创建一个 go.mod 文件。

当项目不在 GOPATH 中,直接执行:

go mod init

否则,会出现如下的错误:

go: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'

因此,我们需要手动激活 Modules:

$ export GO111MODULE=on 

然后才能执行 go mod init。这会将任何现有的dep Gopkg.lock 文件或其他九种支持的依赖关系转换,添加 require 语句以匹配现有配置。

go mod init通常能够使用辅助数据(例如VCS元数据)来自动确定相应的模块路径,但是如果 go mod init 表明它不能自动确定模块路径,或者如果你需要以其他方式覆盖 path,你可以提供模块路径作为 go mod init 的可选参数,例如:

$ go mod init modtest

构建模块

从模块的根目录执行时,./... 模式匹配当前模块中的所有包。 go build 将根据需要自动添加缺失或未转换的依赖项,以满足此特定构建调用的导入:

$ go build ./...

测试模块

$ go test ./...

按配置测试模块,以确保它适用于所选版本。还可以运行模块的测试以及所有直接和间接依赖项的测试以检查不兼容性:

$ go test all

实战

创建项目

创建项目并进入根目录:

$ mkdir src/hello
$ cd src/hello

初始化

$ go mod init github.com/keets2012/hello
go: creating new go.mod: module github.com/keets2012/hello

go mod 初始化,并命名包名为 github.com/keets2012/hello。可以看到,一起创建了 go.mod 文件。

实现一个简单的方法

$ cat <<EOF > hello.go
package main

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
EOF

我们创建了一个 hello.go 的文件,并输出调用的方法结果。

构建执行

$ go build   # 构建可执行文件
$ ./hello  # 执行

Hello, world.  # 输出结果

执行构建之后,得到可执行文件,我们执行了得到结果。 go.mod 文件已更新为包含依赖项的显式版本,其中 v1.5.2semver 标记:

$ cat go.mod

module github.com/keets2012/hello

require rsc.io/quote v1.5.2

升级和降级依赖

应该使用 go get 来完成日常升级和降级依赖项,这将自动更新 go.mod 文件。 或者可以直接编辑 go.mod

此外,像 go buildgo test 或甚至 go list 这样的命令会根据需要自动添加新的依赖项以满足导入。

要查看所有直接和间接依赖项的可用 minor 和 patch 程序升级:

go list -u -m all

要升级到当前模块的所有直接和间接依赖关系的最新版本:

  • 运行 go get -u 以使用最新的次要版本或补丁版本
  • go -u = patch使用最新的补丁版本

要升级或降级到更具体的版本,go get 允许通过在 package 参数中添加@version 后缀或“模块查询”来覆盖版本选择,例如 go get foo@v1.6.2go get foo @ e3702bed2,或者 go foo @'<v1.6.2'

semver

在上一小节,我们提到了 semver。golang 官方推荐的最佳实践叫做 semver,这是一个简称,写全了就是Semantic Versioning,也就是语义化版本。

语义化的定义

通俗地说,就是一种清晰可读的,明确反应版本信息的版本格式,更具体的规范在这里。

如规范所言,形如 vX.Y.Z 的形式显然比一串 hash 更直观,所以 golang 的开发者才会把目光集中于此。

为何使用语义化版本

semver 简化版本指定的作用是显而易见的,然而仅此一条理由显然有点缺乏说服力,通过semver对版本进行严格的约束,可以最大程度地保证向后兼容以及避免 “breaking changes”,而这些都是 golang 所追求的。两者一拍即合,所以 go modules 提供了语义化版本的支持。

如果你使用和发布的包没有版本 tag 或者处于 1.x 版本,那么你可能体会不到什么区别,因为 go mod 所支持的格式从始至终是遵循 semver 的,主要的区别体现在 v2.0.0 以及更高版本的包上。

“如果旧软件包和新软件包具有相同的导入路径,则新软件包必须向后兼容旧软件包。” - go modules wiki

相同名字的对象应该向后兼容,然而按照语义化版本的约定,当出现 v2.0.0 的时候一定表示发生了重大变化,很可能无法保证向后兼容,这时候应该如何处理呢?

答案很简单,我们为包的导入路径的末尾附加版本信息即可,例如:

module my-module/v2

require (
  some/pkg/v2 v2.0.0
  some/pkg/v2/mod1 v2.0.0
  my/pkg/v3 v3.0.1
)

格式总结为 pkgpath/vN,其中 N 是大于 1 的主要版本号。在代码里导入时也需要附带上这个版本信息,如import "some/pkg/v2"。这样包的导入路径发生了变化,也不用担心名称相同的对象需要向后兼容的限制了,因为 golang 认为不同的导入路径意味着不同的包。当然还有意外的情况:

  • 当使用gopkg.in格式时可以使用等价的require gopkg.in/some/pkg.v2 v2.0.0
  • 在版本信息后加上 +incompatible 就可以不需要指定 /vN ,例如:require some/pkg v2.0.0+incompatible

除此以外的情况如果直接使用 v2+ 版本将会导致 go mod 报错。

v2+ 版本的包允许和其他不同大版本的包同时存在(前提是添加了/vN),它们将被当做不同的包来处理。

另外 /vN 并不会影响你的仓库,不需要创建一个v2对应的仓库,这只是 go modules 添加的一种附加信息而已。

当然如果你不想遵循这一规范或者需要兼容现有代码,那么指定 +incompatible 会是一个合理的选择。不过 go modules 不推荐这种行为。

使用 vendor 目录

如果你不喜欢 go mod 的缓存方式,你可以使用 go mod vendor 回到 godep 或 govendor 使用的 vendor 目录进行包管理的方式。

当然这个命令并不能让你从godep之类的工具迁移到 go modules,它只是单纯地把 go.sum 中的所有依赖下载到 vendor 目录里,如果你用它迁移 godep 你会发现 vendor 目录里的包会和 godep 指定的产生相当大的差异,所以请务必不要这样做。

使用 go build -mod=vendor 来构建项目,因为在 go modules 模式下 go build 是屏蔽 vendor 机制的,所以需要特定参数重新开启 vendor 机制:

go build -mod=vendor
./hello
hello world!

构建成功。当发布时也只需要和使用 godep 一样将 vendor 目录带上即可。

总结

本文主要介绍了 go modules的一些特性和使用方法, go modules 是官方的包管理工具,Go 语言通过引入 module 的概念进而引入了 Go tool 的另外一种工作模式 module-aware mode 。在新的工作模式下,module 支持包依赖的版本化管理。

新的工作模式也带来了一些问题,在大陆地区我们无法直接通过 go get 命令获取到一些第三方包,这其中最常见的就是 golang.org/x 下面的各种优秀的包。一旦工作在模块下,go build 将不再关心 GOPATH 或是 vendor 下的包,而是到 GOPATH/pkg/mod 查询是否有cache,如果没有,则会去下载某个版本的 module,而对于某些包的 module,在大陆地区往往会失败。我们将在下篇文章介绍 go module 的 proxy 配置实现。

推荐阅读

Go的包管理工具

订阅最新文章,欢迎关注我的公众号

微信公众号

参考

  1. Modules docs
  2. 再探go modules:使用与细节