一,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@ [email protected]]#pwd
/home/work/jiangfeng01/src/icode.baidu.com/baidu/duer/appserver-common/appserver-3rd/pkg/mod/golang.org/x/[email protected]
[work@ [email protected]]#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@ [email protected]]#
为啥要把这个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/[email protected]
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/[email protected]指定版本号来拉取的。如果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的作用就凸显出来了:对下载的依赖module做安全校验,特别是用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每行内容的格式为:
[/go.mod]
每个引入的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