Go语言module,依赖管理方法
- 1.为什么需要依赖管理
- 2.module管理配置流程
- 打开go module
- 配置一个国内的代理
- 创建go.mod
- Go Modules下的go get行为
- 3.GO env的其他配置
- GOSUMDB
- GONOPROXY/GONOSUMDB/GOPRIVATE
- 4.go.mod和go.sum概述
- 5.Go Modules的语义化版本控制
- 6.Go Modules的最小版本选择
1.为什么需要依赖管理
最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,应该怎么解决?
go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具
2.module管理配置流程
打开go module
要启用go module
支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto
GO111MODULE=off
禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。GO111MODULE=on
启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖。GO111MODULE=auto
当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。
简单来说,设置GO111MODULE=on
之后就可以使用go module
了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。
使用 go module
管理依赖后会在项目根目录下生成两个文件go.mod和go.sum
CMD运行代码:
set GO111MODULE=on
配置一个国内的代理
GOPROXY的值是一个以英文逗号 “,
” 分割的 Go 模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为 “off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理
proxy 顾名思义就是代理服务器的意思。大家都知道,国内的网络有防火墙的存在,这导致有些Go语言的第三方包我们无法直接通过go get命令获取。GOPROXY 是Go语言官方提供的一种通过中间代理商来为用户提供包下载服务的方式。要使用 GOPROXY 只需要设置环境变量 GOPROXY 即可
Windows 下设置 GOPROXY 的命令为:
go env -w GOPROXY=https://goproxy.cn,direct
“direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误
创建go.mod
go mod init [包名]
初始化项目(如果你是初始化项目直接 go mod init
就好了)
此步之后会生成一个go.mod文件
常用的go mod命令如下:
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
Go Modules下的go get行为
在拉取项目依赖时,你会发现拉取的过程总共分为了三大步,分别是 finding(发现)、downloading(下载)以及 extracting(提取)
1、go get的拉取行为
2、go get的版本选择
接下来来研究一下,在 go get 没有指定任何版本的情况下,它的版本选择规则是怎么样的,实际上这需要区分两种情况
①没有发布过 tags
拉取的是 v0.0.0。默认取的是主分支最新一次 commit 的 commit 时间和 commithash
②有发布 tags
在项目有发布 tags 的情况下,还存在着多种模式,也就是只有单个模块和多个模块:
如下是一个多模块项目的tag:
在这个项目中,我们一共打了两个tag,分别是:v0.0.1 和 module/tour/v0.0.1
其实是 Go modules 在同一个项目下多个模块的tag表现方式,其主要目录结构为:
mquote
├── go.mod
├── module
│ └── tour
│ ├── go.mod
│ └── tour.go
└── quote.go
可以看到在 mquote 这个项目的根目录有一个 go.mod 文件,而在 module/tour 目录下也有一个 go.mod 文件
结合上述内容,拉取主模块的话,还是照旧执行如下命令:
go get github.com/eddycjy/mquote@v0.0.1
如果是想拉取子模块,执行如下命令:
go get github.com/eddycjy/mquote/module/tour@v0.0.1
我们将主模块和子模块的拉取进行对比,你会发现子模块的拉取会多出一步,它会先发现github.com/eddycjy/mquote/module
,再继续推算,最终拉取到 module/tour
3.GO env的其他配置
GOSUMDB
它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止
GOSUMDB的默认值为:sum.golang.org
,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理
因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败
一般建议直接设置 GOPRIVATE
,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值
它们的值都是一个以英文逗号 “,
” 分割的模块路径前缀,也就是可以设置多个,例如:
go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。
如果不想每次都重新设置,我们也可以利用通配符,例如:
go env -w GOPRIVATE="*.example.com"
4.go.mod和go.sum概述
1、在初始化项目时,会生成一个 go.mod 文件
一个典型的go.mod文件如下:
module github.com/eddycjy/module-repo
go 1.13
require (
example.com/apple v0.1.2
example.com/banana v1.2.3
example.com/banana/v2 v2.3.4
example.com/pear // indirect
example.com/strawberry // incompatible
)
exclude example.com/banana v1.2.4
replace example.com/apple v0.1.2 => example.com/fried v0.1.0
replace example.com/banana => example.com/fish
- module:用于定义当前项目的模块路径。
- go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本,目前来看还只是个标识作用。
- require:用于设置一个特定的模块版本。
- exclude:用于从使用中排除一个特定的模块版本。
- replace:用于将一个模块版本替换为另外一个模块版本。
另外你会发现 example.com/pear 的后面会有一个 indirect 标识,indirect 标识表示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的,情况有好几种
2、在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件
其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改
github.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM=
github.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=
github.com/eddycjy/mquote/module/tour v0.0.1 h1:cc+pgV0LnR8Fhou0zNHughT7IbSnLvfUZ+X3fvshrv8=
github.com/eddycjy/mquote/module/tour v0.0.1/go.mod h1:8uL1FOiQJZ4/1hzqQ5mv4Sm7nJcwYu41F3nZmkiWx5I=
...
3、查看全局缓存
结果缓存在 $GOPATH/pkg/mod
和 $GOPATH/pkg/sumdb
目录下
需要注意的是同一个模块版本的数据只缓存一份,所有其它模块共享使用。如果你希望清理所有已缓存的模块版本数据,可以执行 go clean -modcache
命令
5.Go Modules的语义化版本控制
我们不断地在 Go Modules 的使用中提到版本号,其实质上被称为“语义化版本”,假设我们的版本号是 v1.2.3,如下:
其版本格式为“主版本号.次版本号.修订号”,版本号的递增规则如下:
- 主版本号:当你做了不兼容的 API 修改。
- 次版本号:当你做了向下兼容的功能性新增。
- 修订号:当你做了向下兼容的问题修正。
假设你是先行版本号或特殊情况,可以将版本信息追加到“主版本号.次版本号.修订号”的后面,作为延伸,如下:
6.Go Modules的最小版本选择
现在我们已经有一个模块,也有发布的 tag,但是一个模块往往依赖着许多其它许许多多的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,如下图(来自Russ Cox):
在上述依赖中,模块 A 依赖了模块 B 和模块 C,而模块 B 依赖了模块 D,模块 C 依赖了模块 D 和 F,模块 D 又依赖了模块 E,而且同模块的不同版本还依赖了对应模块的不同版本。那么这个时候 Go modules 怎么选择版本,选择的是哪一个版本呢?
我们根据 proposal 可得知,Go modules 会把每个模块的依赖版本清单都整理出来,最终得到一个构建清单,如下图(来自Russ Cox):
我们看到 rough list 和 final list,两者的区别在于重复引用的模块 D(v1.3、v1.4),其最终清单选用了模块 D 的 v1.4 版本,主要原因:
- 语义化版本的控制:因为模块 D 的 v1.3 和 v1.4 版本变更,都属于次版本号的变更,而在语义化版本的约束下,v1.4必须是要向下兼容 v1.3 版本,因此认为不存在破坏性变更,也就是兼容的。
- 模块导入路径的规范:主版本号不同,模块的导入路径不一样,因此若出现不兼容的情况,其主版本号会改变,模块的导入路径自然也就改变了,因此不会与第一点的基础相冲突。