深入浅出Go Modules

Go语言从1.11和1.12版本开始引入了初步的modules支持,官方计划是从1.13版本开始默认支持module模式。按Go官方团队的里程碑计划估计,今年8月份1.13会正式发布。

Module是什么

“A module is a collection of Go packages”, 一个模块是一个Go package的集合。

package是什么呢?

如下代码段所示:

import (
  "fmt"
  log "github.com/sirupsen/logrus"
)

fmtgithub.com/sirupsen/logrus 都是package,其中fmt是语言本身包含的package(可以类比为C++的STL),logrus是第三方package。

为什么需要package?

package的引入是为了支持模块化的源代码组织方式,Go程序至少需要一个main package,但应该几乎难以找到一个有用的稍具规模的程序可以不用引入其它package的。

Module的用途

我们思考一个问题,如果项目规模较大,我们引入了第三方包A(v2.0.0)和B (V1.1.0),而B-1.0.0又引入了A-1.0.0。我们通过go get获取的包A的版本只能是确定的一个版本,它不可能同时是v2.0.0和v1.0.0,这时候就需要依赖管理(dependency management) 了。Module的主要工作就是进行依赖管理,在这之前Go语言有一些第三方的依赖管理工具,例如godep, govendor等等。可以说,依赖管理是Go工程的刚需工具,那么在语言官方层面上统一规范是提供支持就是非常有必要的了。

语义化版本

依赖管理处理的其中一个核心问题就是版本升级,如果要在语言官方层面统一依赖管理,那么版本在package的版本管理上统一规范就是很必要的了,Go Module要求遵从语义化的版本规范,关于版本选择的设计详细,可以参考Version Selection

使用Module

读者如果是Module初学者,可以同步在自己的电脑上进行操作。

命令

在命令行中输入go mod help即可查看Module工具有哪些命令。

download edit graph init tidy vendor verify why

路径

因为Module目前还不是语言默认的依赖管理模式(要到Go1.13),所以还需要考虑兼容问题。如果在GOPATH下,Module模式是默认关闭的(即使项目有go.mod文件),需要通过GO111MODULE=on显式的打开。如果不在GOPATH下,则不需要设置该环境变量激活Module模式。另一方面,有Module支持,我们的项目不用统一放到一个GOPATH下或者设置多个GOPATH了。Module管理的模块依赖文件保存在 GOPATH/pkg/mod 目录下。GOPATH/pkg/mod/cache 目录是缓存目录,防止重复下载,其它目录则组织不同版本的模块,例如:

golang.org/x/

crypto@v0.0.0-20190325154230-a5d413f7728c
net@v0.0.0-20190311183353-d8887717615a
net@v0.0.0-20190503192946-f4e77d36d62c
net@v0.0.0-20190522155817-f3200d17e092
net@v0.0.0-20190620200207-3b0461eec859

init

假设我们在GOPATH外新建了hello目录,其中有hello.go和hello_test.go文件。文件内容分别如下:

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)
    }
}

进入到hello目录,运行go mod init example.com/hello命令将该目录作为一个module的根目录进行初始化,初始化结束后hello目录新增了go .mod文件,文件内容为:

module example.com/hello
go 1.12

表示在该根目录声明了 example.com/hello 模块,使用的是Go版本是1.12,此后如果要新增子目录创建新的package,则package的导入路径自动为module名加子目录名。

创建morning/morning.go, 在hello.go中导入该package的路径为 import example.com/hello/morning

我们修改hello.go导入一个外部模块:

package hello

import "rsc.io/quote"

func Hello() string {
  return quote.Hello()
}

运行go test,第一次运行可能会比较慢,因为需要先下载相关文件。PASS后,查看go.mod文件:

module example.com/hello

go 1.12

require rsc.io/quote v1.5.2

我们可以看到在文件末尾新增了一条依赖声明,定义了依赖的模块(rsc.io/quote)及其版本号(v1.5.2)。还可以看到新增了一个go.sum文件,这是一个校验文件,不需要人工维护,是工具用于判断依赖是否发生变化。

查看所有依赖模块

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

该命令列出当前模块依赖的所有模块,可以看出,除了 rsc.io/quote v1.5.2 外,还间接依赖其它模块(rsc.io/quote v1.5.2所依赖的)。

版本升级

升级到最新版本

通过上一个命令,我们知道当前模块依赖的golang.org/x/text模块版本是v0.0.0,我们想尝试一下将它升级到最新版本是否兼容。运行: go get golang.org/x/text

升级后的go.mod文件:

module example.com/hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
)

升级后,运行go test,PASS代表升级成功。

我们再升级一下rsc.io/sampler,go get rsc.io/sampler,再运行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.006s

升级失败,说明升级到最新的rsc.io/sampler与当前模块不兼容,如果rsc.io/sampler严格遵守语义化版本]1规范,则我们升级到一个兼容版本v1.3.z (z > 0)是应该可以通过测试的。

升级到指定版本

我们先看看它有哪些已发布版本: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.1: go get rsc.io/sampler@v1.3.1, 继续测试go test, 测试通过!再检查go.mod文件:

module example.com/hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
	rsc.io/sampler v1.3.1 // indirect
)

添加一个新的major版本依赖

当遇到依赖同一模块的不同版本需求时,我们可以这样解决:

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

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)
    }
}

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

因为是同一个模块,所以我们将v3版本导入为quoteV3。运行go test,go.mod文件有如下变更:

module example.com/hello

go 1.12

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

列出当前当前模块(example.com/hello)依赖的rsc.io/quote模块的所有版本:

go list -m rsc.io/q..., 结果为:

rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0

升级到不兼容的版本

按照语义化版本规则,major版本的变更,意味着接口不兼容。如果还要使用新版本依赖,那就要求我们修改自己的代码去显式的使用新版本。这里,我们将模块对rsc.io/quote的所有依赖都升级到v3,删除对低版本的依赖,修改hello.go:

package hello

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

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

运行go test, PASS。但是查看go.mod文件,依然包含 rsc.io/quote v1.5.2 ,此时我们运行go mod tidy命令清理不再使用的依赖,清理后再查看go.mod文件,已经不再包含 rsc.io/quote v1.5.2

vendor

运行go mod vendor命令将在当前模块根目录生成vendor目录,将module的依赖都拷贝到该目录下。这样做主要有两个好处:

  1. 保证依赖的所有模块都可以重复获取到,例如防止第三方作者将自己的开源项目删除。
  2. 提高CI工具的效率,有了vendor目录,go build时就不用再重新去下载。

当然,这也是有坏处的,那就是如果将vendor目录也提交到版本控制中,则会增加项目大小,增加管理复杂度。

发布Module

如果要将自己的module发布供他人使用,则需要遵守一定的规范,具体参考Go官方文档

总结

go.mod和go.sum文件都应该纳入到版本控制系统中,Go Module要依赖这两个文件工作。Go Modules核心要解决的问题就是可靠的重复构建,这是一个确定的工程需求。

Contents