go module使用揭秘

一,module的来源定义

go1.11和go1.12对golang的 module做了一个试水,从go1.13开始,go默认使用module来关系包依赖关系。本篇讲的是如何在一个新项目中使用module。如果读者关注把一个已有的项目迁移到用module进行依赖管理,请移步migrating-to-go-modules

一个module包含一个或多个go pkg,且在根目录下有一个go.mod文件:

project
│   go.mod
│   ...
│
└───pkg1
│   │   1.go
│   │   2.go
│   │    ...
│   └───pkg1_1
│       │   1.go
│       │   2.go
│       │   ...
│   
└───pkg2
    │   1.go
    │   2.go

这个project我们就称之为一个module。

二,go mod init命令新建一个module

首先,go modules设置为auto(1.13默认是auto)或者on,然后在GOPATH/src目录之外新建一个目录hello, cd 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目录还不是一个module,因为hello根目录还没有go.mod文件。然后我们运行go test来对hello.go跑单测:

[work@ hello]#go test
PASS
ok      _/home/work/minping/hello   0.002s
[work@ hello]#

最后一行输出是go test的全部概览信息,ok表示所有测试用例成功,最后0.002s是运行时间,但是中间一行_/home/work/minping/hello代表什么?
原来此时的hello目录不在任何$GOPATH/src目录下,也不在某个module目录下(hello目录目前还没有go.mod),如果其他包想引入,是引入不了的。此时go test工具链依据当前目录给了个虚拟的module name _/home/work/minping/hello

我们使用 go mod init来创建一个module:

[work@ hello]#go mod init example.com/hello
go: creating new go.mod: module example.com/hello
[work@ hello]#

看到生成了一个go.mod文件:

[work@ hello]#cat go.mod
module example.com/hello

go 1.13
[work@ hello]#

go.mod的module标示了此module的name是example.com/hello。此时如果第三方包想引入我们的hello包,使用hello.go中的Hello函数,只需要import example.com/hello即可:

package min
import(
    "fmt"
    "example.com/hello"
)


func get(){
    h := hello.Hello()
    fmt.Println(h)
}

如果我们的module是这样的:

hello
│   go.mod
│   hello.go
│   hello_test.go
│
└───world
   │   world.go
   │   world_test.go

第三方包想引入子pkg world中world.go文件中的World函数,也很方便,把module name和子pkg的名称拼起来module_name+pkg_name就行,而不需要在world目录下面再用go mod init来创建一个world的module:

package min
import(
    "fmt"
    "example.com/hello"
    "example.com/hello/world"
)


func get(){
    h := hello.Hello()
    w := world.World()
    fmt.Println(h, w)
}

如上demo,当import "example.com/hello/world"时,go module会首先找module_name="example.com/hello/world"的module,如果没有的话,就退一层找module_name="example.com/hello"的module,找到后,再在example.com/hello这个module里面找名为world的子pkg。

三,新增一个dependency

当我们需要用某个第三方包中某些函数或变量时,我们需要import这个第三方包。修改hello.go:

//hello.go
package hello

import "rsc.io/quote"

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

运行go test:

[work@ 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
[work@ hello]#

可以看到go test命令运行时,会自动拉取我们的依赖rsc.io/quote以及子依赖。其实,当我们import某个pkg,这个pkg不在go.mod列表中时,go工具链会自动拉取包含这个pkg的module,并将此module加入到go.mod文件中。注意是把module name写入到go.mod,而不是把pkg名写到go.mod中。验证如下,我们找一个第三方包,go.mod在根目录下,子pkg我们拿来用:

//hello.go
package hello

import (
    "golang.org/x/text/unicode/bidi"
    "rsc.io/quote"
)

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

func Bidi() int {
    return int(bidi.LeftToRight)
}

其中golang.org/x/text是一个module,unicode/bidi是这个module里面的子pkg,我们想使用bidi包里面的LeftToRight,写了如上demo,运行后发现go.mod内容如下:

[work@ hello]#cat go.mod
module example.com/hello

go 1.13

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

可以看到require中记录的是golang.org/x/text这个module名,而不是我们import的golang.org/x/text/unicode/bidi这个pkg名。然后在$GOPATH/pkg/mod目录下可以看到拉取了golang.org/x/text这个module的全部内容:

[work@ text@v0.3.2]#pwd
/home/work/jiangfeng01/src/icode.baidu.com/baidu/duer/appserver-common/appserver-3rd/pkg/mod/golang.org/x/text@v0.3.2
[work@ text@v0.3.2]#ll
总用量 120
-r--r--r--   1 work work  173  1月 17 16:48 AUTHORS
dr-xr-xr-x   2 work work 4096  1月 17 16:48 cases
dr-xr-xr-x   3 work work 4096  1月 17 16:48 cmd
...
-r--r--r--   1 work work 9048  1月 17 16:48 gen.go
-r--r--r--   1 work work   88  1月 17 16:48 go.mod
-r--r--r--   1 work work  211  1月 17 16:48 go.sum
[work@ text@v0.3.2]#

为啥要把这个module的全部内容下载下来,而不是只下载import的unicode/unicode/bidi和它依赖的子pkg,不是很理解??maybe go认为依赖的最小对象单元是module,而不是pkg,我试了一些,go get golang.org/x/text/unicode/bidi也是下载golang.org/x/text这整个module,而不是golang.org/x/text/unicode/bidi这个pkg及依赖pkg。当然我找的golang.org/x/text是用go module管理的,不知道go get那些没有用go module管理的老pkg是什么结果,有兴趣的可以试试。

回到正题,第一次go test会将下载的依赖放在$GOPATH/pkg/mod目录下缓存起来,第二次go get就不会重复下载依赖了。

在go.mod中我们可以看到当前module直接依赖了哪些module,如果要看所有的依赖(直接+间接),可以用go list -m -all命令:

[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
[work@ hello]#

go list -m all输出结果中,第一行是当前module的名称,从第二行开始就是依赖的module name和版本号。

四,依赖module的版本号

上面可以看到依赖的golang.org/x/text这个module的版本信息是v0.3.2(第三方module做好打包生成的版本号),golang.org/x/tools的版本信息是v0.0.0-20180917221912-90fa682c2a6e(第三方module没有打包时,golang根据pseudo-version规则生成版本信息)

Q1:如何查看某个依赖module有哪些版本可用
分两种情况,对于v2以下的module,直接使用go list -m -versions module_name

[work@ hello]#go list -m -versions rsc.io/quote
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1
[work@ hello]#
[work@ hello]#go list -m -versions github.com/kataras/iris
go: finding github.com/kataras/iris v11.1.1+incompatible
github.com/kataras/iris v8.2.1+incompatible v8.3.4+incompatible v8.4.0+incompatible v8.4.2+incompatible v8.4.3+incompatible v8.4.4+incompatible v8.4.5+incompatible v8.5.0+incompatible v8.5.2+incompatible v8.5.4+incompatible v8.5.7+incompatible v8.5.9+incompatible v10.0.0+incompatible v10.1.0+incompatible v10.2.1+incompatible v10.3.0+incompatible v10.4.0+incompatible v10.5.0+incompatible v10.6.0+incompatible v10.6.3+incompatible v10.6.4+incompatible v10.6.5+incompatible v10.6.6+incompatible v10.6.7+incompatible v11.1.0+incompatible v11.1.1+incompatible
[work@ hello]#

可以看到rsc.io/quote这个moudule有v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1这几个版本可选。
github.com/kataras/iris这module的v2以下tag和preleleasedtag没有版本可选,untagged版本有v8.2.1+incompatible v8.3.4+incompatible ... v11.1.1+incompatible这些可选。

Q2:go test,go run等工具链是怎么拉取某个module依赖的?
go test,go run等工具链会先检查要import的module在go.mod中有没有列出来

  • go.mod中列出来的,直接按照go.mod中这个依赖module的版本拉取依赖
  • go.mod中没有列出来,工具链会自动查找包含我们要import的pkg的module,把找到的module写到go.mod并拉取下来

Q3:关于版本号的格式是什么样的,tagged stable version 的格式? tagged prerelease version的格式?untagged version版本号的格式?

  • tagged stable version格式为vX.Y.Z (Major.Minor.Patch),遵从Semantic Version规则
  • tagged prerelease version的格式一般为vx.y.z-pren,比如我们看rsc.io/quote这个module的一个prerelease version为v1.5.3-pre1
  • latest untagged version,golang自主定义pseudo-version格式的,见前面介绍。

在下面的例子中可以看到tagged stable version,tagged prerelease version和latest untagged version示例:

[work@ hello]#go list -m -versions rsc.io/quote
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1
[work@ hello]#
[work@ hello]#go list -m -versions golang.org/x/tools
golang.org/x/tools
[work@ hello]#

看到rsc.io/quote有tagged stable version v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2和一个tagged prerelease version v1.5.3-pre1,但是golang.org/x/tools没有任何tagged版本,所以go给生成了一个pseudo-version,我们可以查看最终用的版本信息:

[work@ hello]#go list -m golang.org/x/tools
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
[work@ hello]#

Q4:go test,go run等工具链是怎么知道要拉取哪个特定版本的module的?

  • 如果import的module没有带版本,比如rsc.io/quote,则工具链拉取此module的v2以下的版本,并按如下规则优先选择拉取:
    • 优先取latest tagged stable version
    • 否则优先取latest tagged prerelease version
    • 否则优先取latest untagged version
  • 如果import的module带了版本号,比如rsc.io/quote/v3,则工具链拉取v3的最新版本
    我们以github.com/kataras/iris这个module为例,说明go module工具是如何获取某个moudule的可用版本的。
    首先我们试一下不带版本号的github.com/kataras/iris:
[work@ hello]#go list -m -versions github.com/kataras/iris
go: finding github.com/kataras/iris v11.1.1+incompatible
github.com/kataras/iris v8.2.1+incompatible v8.3.4+incompatible v8.4.0+incompatible v8.4.2+incompatible v8.4.3+incompatible v8.4.4+incompatible v8.4.5+incompatible v8.5.0+incompatible v8.5.2+incompatible v8.5.4+incompatible v8.5.7+incompatible v8.5.9+incompatible v10.0.0+incompatible v10.1.0+incompatible v10.2.1+incompatible v10.3.0+incompatible v10.4.0+incompatible v10.5.0+incompatible v10.6.0+incompatible v10.6.3+incompatible v10.6.4+incompatible v10.6.5+incompatible v10.6.6+incompatible v10.6.7+incompatible v11.1.0+incompatible v11.1.1+incompatible
[work@ hello]#
[work@ hello]#go get github.com/kataras/iris
go: downloading github.com/kataras/iris v11.1.1+incompatible
go: extracting github.com/kataras/iris v11.1.1+incompatible
go: finding github.com/iris-contrib/formBinder v5.0.0+incompatible
go: finding gopkg.in/yaml.v2 v2.2.2
go: finding github.com/fatih/structs v1.1.0
go: finding github.com/klauspost/compress v1.8.3
go: finding golang.org/x/crypto latest
go: finding github.com/iris-contrib/go.uuid v2.0.0+incompatible
go: finding github.com/kataras/golog v0.0.9
go: downloading github.com/klauspost/compress v1.8.3
go: finding github.com/ryanuber/columnize v2.1.0+incompatible
go: finding github.com/microcosm-cc/bluemonday v1.0.2
go: downloading golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
go: downloading github.com/iris-contrib/formBinder v5.0.0+incompatible
^C
[work@ hello]#

当github.com/kataras/iris这个module不带版本号时,我们首先拉取v0.y.z,v1.y.z这种tagged stable version,发现没有,然后go module就拉取tagged prerelease version这种版本,也没有,最后才拉取untagged version,于是我们就看到go list显示的一堆incompatible版本,然后go get时那的也是这里面最新的版本v11.1.1+incompatible。

然后我们试一下带版本号的github.com/kataras/iris/v12:

[work@ hello]#go list -m -versions github.com/kataras/iris/v12
github.com/kataras/iris/v12 v12.0.0 v12.0.1
[work@ hello]#
[work@ hello]#go get github.com/kataras/iris/v12[work@ hello]#go mod init example.com/hello
go: creating new go.mod: module example.com/hello
[work@ hello]#go get github.com/kataras/iris/v12
go: downloading github.com/kataras/iris/v12 v12.0.1
go: extracting github.com/kataras/iris v11.1.1+incompatible
go: extracting github.com/kataras/iris/v12 v12.0.1
go: downloading github.com/BurntSushi/toml v0.3.1
go: downloading github.com/kataras/golog v0.0.9
go: downloading github.com/klauspost/compress v1.9.0
^C
[work@ hello]#

当go get github.com/kataras/iris/v12这个module带版本号v12时,我们也是首先拉取v12.y.z这种tagged stable version和v12.y.z-pren这种tagged prereleased version,发现有v12.0.0 v12.0.1这几种version可选,然后我们用go get github.com/kataras/iris/v12,发现果然拉取的是
v12.0.1。

五,升级依赖的module的minor版本

go get module_name 可以在保持major版本不变的情况下,升级minor版本。下面给出示例。
当前依赖module版本信息如下:

[work@ hello]# 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
[work@ hello]#

可以看到golang.org/x/text的版本号是一个伪版本号,我们使用go list -m -versions来看一下:

[work@ hello]#go list -m -versions golang.org/x/text
golang.org/x/text v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.3.2
[work@ hello]#go list -m -versions golang.org/x/tools
golang.org/x/tools
[work@ hello]#
[work@ hello]#go list -m -versions rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99

发现golang.org/x/text在v2以下有v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.3.2这几个版本可选。而golang.org/x/tools没有可用版本。rsc.io/sampler有
v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99这几个版本。我们给golang.org/x/text和rsc.io/sampler这两个module升级:

[work@ hello]# 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
[work@ hello]#
[work@ hello]# 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
[work@ hello]#

我们发现golang.org/x/text升级后ok,但是rsc.io/sampler从v1.3.0升级到v1.99.99之后,go test运行时报错:

[work@ hello]#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
[work@ hello]#

虽然v1.3.0升级到v1.99.99,major版本不变,按照Semantic Version规范,v1.99.99应该是兼容v1.3.0的,但有些不规范的第三方开发者就是可能不遵守规则,导致不兼容的升级。现在我们要解决不兼容升级的问题,首先查看rsc.io/sampler有哪些可用版本:

[work@ hello]#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
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
[work@ hello]#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
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.1
[work@ hello]#

首先查找rsc.io/sampler可用版本,从最后往前面找,1.99.99不兼容,那么我们拉取v1.3.1。go test运行ok。说明v1.3.1可用且兼容v1.3.0
这里我们是用go get rsc.io/sampler@v1.3.1指定版本号来拉取的。如果go get rsc.io/sampler这样后面不带版本号,则默认是拉取go get rsc.io/sampler@latest

六,增加module的某个不同major版本

设想这个场景:rsc.io/quote有v1.5.2和v3.1.0两个不同的major版本,我们代码的某个部分A使用rsc.io/quote v1.5.2的Hello接口(Hello接口在v3.1.0版本被移除或者不兼容升级), 另外一个部分B又需要使用版本v3.1.0新加的一个Concurrency接口。这种情况怎么解决需要同时存在两个以上不同major版本的module呢?

官方解答是,每个不同的major版本module都需要有不同的import path。对于v2以下的module,直接import module_name,对于v2或以上的module,必须要用 module_name/major_version这样的格式来引入。比如,rsc.io/quote这个module的v2以下版本可以用import rsc.io/quote来引入,但是v3版本的必须要用rsc.io/quote/v3来引入。这个做法我们称为语义版本导入。好处很显然:

  • 我们可以在同一个module中引入不同major版本的rsc.io/quote。
  • 我们可以按需要只升级v1 版本的rsc.io/quote,或者只升级v3 版本的rsc.io/quote,而不必全部升级所有的v1和v3版本rsc.io/quote,在大型项目或者基础库中,这个需求更常见。
    rsc.io/quote的v1和v3版本的代码共存示例:
//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()
}

go test后看下go.mod:

[work@ hello]#cat go.mod
module example.com/hello

go 1.13

require (
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.1.0
)
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.0
[work@ hello]#

从go.mod和go list -m都可以看到,rsc.io/quote两个版本v1.5.2和v3.1.0都包含在内。我们可以用go get rsc.io/quote来单独升级v1版本的rsc.io/quote module,或者可以用go get rsc.io/quote/v3来单独升级v3版本的rsc.io/quote module。

go 1.13

require (
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.1.0
)
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.0
[work@ hello]#

七,升级module的major版本

某些第三方库可能过段时间就发布更高的major版本来增加某些新接口,也可能会对现有接口做不兼容的升级(虽然golang官方极力推荐大家做兼容升级,建议发布新major版本时不要对原有接口做重命名等不兼容升级,而是新增接口)。比如rsc.io/quote这个module发布的v3版本,已经把v1版本的Hello接口改成了Hellov3接口,并新增了Concurrency接口,恰好我们的项目中需要Concurrency接口,我们又不想像上一节那样在同一个module中同时import v1和v3两个版本的module,此时我们只有把依赖的rsc.io/quote从v1升级到v3版本。
首先我们看以下v1和v3版本的rsc.io/quote输出能力文档:

[work@ hello]#go doc rsc.io/quote
package quote // import "rsc.io/quote"

Package quote collects pithy sayings.

func Glass() string
func Go() string
func Hello() string
func Opt() string
[work@ hello]#
[work@ hello]#
[work@ hello]#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
[work@ hello]#

go doc结果的第一行就告诉我们如果要引用此module,要用import "rsc.io/quote/v3"来引入。注意这里go1.13之前的版本有一个bug,把major版本截断去掉了,这个bug在go1.13得到修复。
从doc中看到,在v3版本中,Hello接口已经rename成HelloV3。我们要做的是,将import rsc.io/quote更换成import rsc.io/quote/v3,并且在代码中用到
Hello的地方改成HelloV3。代码如下:

//hello.go
package hello

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

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

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

可以看到,我们已经移除了rsc.io/quote,只有rsc.io/quote/v3,运行go test后ok,我们用go.mod和go list -m看下依赖module:

[work@ hello]#cat go.mod
module example.com/hello

go 1.13

require (
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.1.0
)
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.0
[work@ hello]#

发现go.mod和go list -m中都显示还是同时引用了rsc.io/quote v1.5.2和rsc.io/quote/v3 v3.1.0,但是我们的代码现在没有引用rsc.io/quote v1.5.2,这是怎么回事?

七,清理没有使用的依赖module

上一节提到,rsc.io/quote没有被使用,却还在go.mod中,会给我们整理依赖关系造成干扰。为什么go test 不主动做移除动作?因为我们现在只是go test单个pkg,而go.mod是对整个module的引入依赖描述,所以go test命令监测到go.mod中的rsc.io/quote没有被当前pkg用到,并不会清理go.mod中的rsc.io/quote,因为不排除其他pkg用到了rsc.io/quote。只有检查了当前module中所有pkg的依赖都没有用到rsc.io/quote之后,才可以把rsc.io/quote从go.mod中安全的移除。一般的go 工具链命令不会做这种全局检查,包括go test,go build,go install,go run等命令。

go mod tidy工具为此设计。go mod tidy会检查当前module的所有依赖关系,然后从go.mod中移除没用到的module。

[work@ hello]#go mod tidy
[work@ hello]#cat go.mod
module example.com/hello

go 1.13

require rsc.io/quote/v3 v3.1.0
[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.0
[work@ hello]#

可以看到go mod tidy后,rsc.io/quote已经从go.mod中移除。需要注意的是,go tidy也会将pkg中引入的但go.mod中没有的module写入到go.mod中。

八,关于go.sum

之前一直以为go.sum里面的内容human unreadable,只是每次根据go.mod和各子依赖的go.mod自动生成的一个文件,一度认为它不应该加入到version control。

说起go.sum的作用,我们得从goproxy和依赖module下载说起。我们使用golang工具链下载依赖module源码时,其实是从GOPROXY这个环境变量定义的代理站点来下载的。GOPROXY默认值是"https://proxy.golang.org,direct":

[work@ hello]#go env GOPROXY
https://proxy.golang.org,direct
[work@ hello]#

上面的GOPROXY设置的意思是,当下载rsc.io/sampler这个依赖module时,会首先从https://proxy.golang.org这个站点查找有没有rsc.io/sampler,如果这个站点没找到rsc.io/sampler这个module(http返回状态码404 or 410)时,则使用direct,即会直接访问rsc.io/sampler这个站点。GOPROXY的格式是,各个站点url之间用逗号连接来定义的。比如默认的https://proxy.golang.org,direct。你可以理解为direct也是一个特殊的"站点",此时go工具链会从这些proxy站点中从前到后逐个尝试拉取需要的module代码。注意放在direct后面的proxy站点都是无效的。还有一个特殊站点为"off",即GOPROXY=off,表示禁止从任何站点拉取源码。

GOPRIVATE和GONOPROXY环境变量中定义的module会忽略GOPROXY的设置,即相当于direct,会尝试直接从source control servers(源代码控制服务器)访问rsc.io/sampler这个module。

当从远程拉取依赖module时,go工具链会首先从go.sum中找到对于module的checksum信息,和拉取下来的module源码做checksum对比。如果go.sum中没有此module信息,则从Go checksum数据库中找到并更新到go.sum中。Go checksum数据库的地址由GOSUMDB 和 GONOSUMDB这两个环境变量设置。参考来源

那么go.sum的作用就凸显出来了:<b>对下载的依赖module做安全校验</b>,特别是用go proxy做代理之后。

首先我们看下go.sum内容:

[work@ hello]#go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.0
[work@ hello]#
[work@ hello]#cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
[work@ hello]#

可以看到,go.sum每行内容的格式为:

<module> <version>[/go.mod] <hash>

每个引入的module在go.sum中都有两行:

  • 第一行是对这个module的整个文件夹内容的hash结果
  • 第二行是对这个module中go.mod这个文件的hash结果

如何做校验?

go test,go build等这些golang工具链命令在拉取的依赖module放在GOPATH/pkg/mod/cache/download目录的同时,会对每个pkg做checksum记录到文件中:

[work@ @v]#ls -all
总用量 40
drwxrwxr-x  2 work work  4096  1月 19 14:38 .
drwxrwxr-x  3 work work  4096  1月 19 14:38 ..
-rw-rw-r--  1 work work     7  1月 19 14:38 list
-rw-rw-r--  1 work work     0  1月 19 14:38 list.lock
-rw-rw-r--  1 work work    50  1月 19 14:38 v1.3.0.info
-rw-rw-r--  1 work work     0  1月 19 14:38 v1.3.0.lock
-rw-rw-r--  1 work work    88  1月 19 14:38 v1.3.0.mod
-rw-------  1 work work 14308  1月 19 14:38 v1.3.0.zip
-rw-rw-r--  1 work work    47  1月 19 14:38 v1.3.0.ziphash
[work@ @v]#pwd
/home/work/jiangfeng01/src/icode.baidu.com/baidu/duer/appserver-common/appserver-3rd/pkg/mod/cache/download/rsc.io/sampler/@v

在go test等工具链命令执行的时候,那我们的hello项目和依赖的rsc.io/sampler这个module为例子:
1)首先会找hello这个module下的go.sum文件,找到rsc.io/sampler这个依赖module的checksum信息:

[work@ hello]#cat go.sum |grep "rsc.io/sampler"
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
[work@ hello]#

2)再在mod/cache/download目录下找到下载此依赖时,记录在ziphash文件中的hash信息:

[work@ @v]#cat v1.3.0.ziphash
h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
[work@ @v]#
[work@ @v]#pwd
/home/work/jiangfeng01/src/icode.baidu.com/baidu/duer/appserver-common/appserver-3rd/pkg/mod/cache/download/rsc.io/sampler/@v

发现两者一致,校验通过。如果某天go build构建的时候,发现go.sum中记录的某个module的checksum和cache/download中记录的hash不一致,则golang工具链命令会报告一个安全错误,并终止golang工具链命令的执行。此时我们需要查清楚到底是go.sum中记录的checksum不对,还是cache/download中记录的checksum不对,通常情况下,go.sum是没有问题的,你需要查找原代码是哪里改的不对,是不是意外更改导致cache/download中的checksum发生变化,而且一般情况下需要回退使用更改之前的代码。
我们可以使用go mod verify命令手动检查两者的checksum是否一致:

[work@ hello]#go mod verify
golang.org/x/text v0.3.0: missing ziphash: open /home/work/jiangfeng01/src/icode.baidu.com/baidu/duer/appserver-common/appserver-3rd/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.ziphash: no such file or directory
[work@ hello]#

结果显示,cache/download目录中golang.org/x/text这个module没有记录checksum,但是go.sum中却有这个module的checksum,为什么?是不是跟这个module没有用module管理有关?

go.sum存在的意义:对下载的依赖module做安全校验,特别是用go proxy做代理之后。

参考

Using Go Modules

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271