Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,Go1.14 上已经明确建议生产上使用了。而 Go modules 之前,Go 项目使用 GOPATH 模式,先讨论下 GOPATH 模式的问题。
GOPATH 的前世今生
GOPATH 是布置 Go 开发环境时所设置的一个环境变量。历史版本的 go 语言开发时,需要将代码放在 GOPATH 目录的 src 文件夹下。go get 命令获取依赖,也会自动下载到 GOPATH 的 src 下。
例如:
可以通过命令 go env 获取当前设置的 GOPATH 路径。
GOPATH 具体结构如下,必须包含三个文件夹,具体如下图所示:
GOPATH 模式不再推荐的原因
最严重的问题就是,GOPATH 模式下,go get 命令使用时,没有版本选择机制,拉下来的依赖代码都会默认当前最新版本,而且如果当项目 A 和项目 B 分别依赖项目 C 的两个不兼容版本时, GOPATH 路径下只有一个版本的 C 将无法同时满足 A 和 B 的依赖需求。这可以说是一个很大的缺陷了,因而 Go1.13 起,官方就不再推荐使用 GOPATH 的模式了。
GOPATH 模式衍生出的版本管理工具的进化
随着 Go 语言使用人数的增长,依赖包的丰富,依赖版本问题尤其严重。
于是 Go 官方在 Go 1.5 的时候提出了实验性质的 vendor 机制:每个项目都可以有一个 vendor/ 目录来存放项目所需版本依赖的拷贝。
社区中基于官方给的机制,开发出了各种版本管理工具。比较流行的比如 govendor,以及之前曾被官方认定的 godep 工具等。
这些工具的思路基本都是为每个项目单独维护一份对应版本依赖的拷贝。
管理工具虽然丰富了起来,但是不同版本工具之间不兼容,无法协作,各种工具还都有学习成本。这时候 在 Go 官方扶持下成立的 dep 项目被大家认为是未来一统江湖的版本管理工具,被称作 official experiment。
dep 采用了和 Rust 的管理工具 Cargo 类似的管理模式,原理在此不深究。
没过多久,Go 社区的核心人物 rsc 提出了 vgo 方案。一时间竟然出现了两个所谓的 Go 官方的版本管理方案。最终官方采用了 vgo 方案,随着 vgo 的逐渐成熟,Go 1.11 发布了该功能,并集成到了 Go 的官方工具中,也就是当前的 Go modules。
Go modules 相关环境变量
使用 Go modules,一般涉及到如下 6 个环境变量的配置,go env 可以列出,摘录如下:
GO111MODULE
是开启使用 Go modules 的开关,可用参数值如下:
设置开启 on 的方法
也可在 .bash_profile 文件中直接环境变量方式设置,增加
GOPROXY
用于设置 Go 模块代理,其作用是使 Go 在拉取模块版本时直接通过镜像站点来快速拉取默认值是这个默认地址,国内无法访问,所以开启 Go Modules 必需设置镜像代理地址
列举国内常用的镜像代理地址如下:
GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理
不使用,可设为 “off”
默认值中有个 direct,用来告诉 Go 将直接从依赖的源地址进行下载操作(比如 GitHub 等),例如当值列表中上一个 Go 代理返回 404 或 410 时,Go 自动尝试列表中的下一个,遇见 “direct” 时回到源地址去抓取。
最终:设置代理方法一般如下:如使用七牛网代理
也可在 .bash_profile 文件中直接环境变量方式设置,增加
GOSUMDB
Go checksum database 的缩写,含义如其名字,用于在拉取模块版本时保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。
GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理,即设置 GOPROXY 自然就解决了。
若设置为“off”,就禁止 Go 在后续操作中校验模块版本。
GONOPROXY / GONOSUMDB / GOPRIVATE
如果当前项目依赖了私有模块,则配置会涉及这三个环境变量。例如公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。简单来说就是应对,GOPROXY 设置的代理或 GOSUMDB 设定的 Go checksum database 代理无法访问模块时的情形。
建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳设置是直接使用 GOPRIVATE。
它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:
设置后,前缀为 *.my.com 和 github.com/eabc/def 的模块都会被认为是私有模块。
将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 my.com 本身。
Go modules 操作命令及相关文件解读
可以命令行执行 go help mod 打印出 go mod 相关命令
接下来主要介绍 go mod init 命令,及其涉及到的必要文件
go mod init 命令
初始化项目,使用方法例子如下:创建个空项目之后执行如下命令
指定了模块导入路径为 gitee.com/biexiaoyansudian/my.cn
这个 init 指定的路径作用是:
作为模块的标识(identity)
作为模块的 import path,当其他项目引用这个模块下的 package 时都会以该 import path 作为共同的前缀,比如:
go mod init 执行完毕,就初始化了使用 Go modules 的项目,会多出来一个 go.mod 文件。它记录了当前项目的模块信息,每一行都以一个关键词开头。
go.mod 文件
此时,go.mod 文件内容如下
Go modules 模式下,使用 go get 命令,相关信息可以自动记录到 go.mod 文件中
看如下示例:
在刚才初始化的项目中,下载 gorm 扩展包
默认下载最新版本,Go modules 模式下 go get 可以进行版本指定 @ 版本管理的 tag,例如
其拉取的结果缓存在 GOPATH/pkg/sumdb 目录下,而在 mod 目录下会以 github.com/foo/bar 的格式进行存放。
下载完成后,go.mod 文件内容自动变更如下
下面介绍下 go.mod 文件中可以使用到的语法关键词以及含义:
module: 定义当前项目的模块路径,不再赘述
go: 标识当前模块的 Go 语言版本,目前来看还只是个标识作用。
require: 说明 Module 需要什么版本的依赖。
exclude: 用于从使用中排除一个特定的模块版本。在实际的项目中很少被使用,故很少会显式的排除某个包的某个版本,除非我们知道某个版本有严重 bug。比如指令 exclude github.com/google/uuid v1.1.0,表示不使用 v1.1.0 版本。
场景举例:
当前项目 gitee.com/biexiaoyansudian/my.cn
下载依赖 go get github.com/google/uuid@v1.0.0
my.cn 的 go.mod 文件如下
此时另创建项目 gitee.com/biexiaoyansudian/exclue
下载依赖 go get -uu github.com/google/uuid@v1.1.0
此时如果当前 my.cn 项目想要引入 exclue 项目,可以看到 exclue 项目用的是 uuid@v1.1.0 大于 my.cn 项目版本,则会自动的将 my.cn 项目的也升级为 uuid@v1.1.0
my.cn 的 go.mod 文件如下
而如果在下载 exclue 依赖之前,在 my.cn 项目的 go.mod 文件里加上
此时,再去拉 exclue 项目,可以发现 uuid 直接跳过了 v1.1.0 版本升级到了 v1.1.1
my.cn 的 go.mod 文件如下
replace: 替换 require 中声明的依赖,使用另外的依赖及其版本号。
首先介绍个命令,可以查看项目最终所使用的全部依赖
使用场景一:替换 require 的包
用例子来说明 replace 的使用场景一,一般也是最不会使用的场景。
当前项目的 go.mod 如下
下执行命令 go list -m all
我们在 go.mod 下增加这样一句配置
再次执行命令,结果如下
发现最终生效的由 uuid v1.1.1 被替换成了 uuid v1.1.0
一般来说,这种场景使用的不多,和直接修改 require 作用相同
生效有前提条件:
一是只有在当前引用的模块有效
二是 replace 命令左侧的包名和版本,必须是 require 中包含的包名和版本
场景二:替换无法下载的包
由于网络问题,有些包无法下载,比如 golang.org 下的包,而这些包在 GitHub 都有镜像,此时就可以使用 GitHub 上的包来替换。
比如,项目中使用了 golang.org/x/text 包:
项目编译时就会从 GitHub 下载包。我们源代码中 import 路径 golang.org/x/text/xxx 不需要改变
场景三:调试依赖包
我们调试依赖包,就可以使用 replace 来修改依赖,如下所示:
使用本地的 uuid 来替换依赖包,此时,我们可以任意地修改 …/uuid 目录的内容来进行调试。
除了使用相对路径,还可以使用绝对路径,甚至还可以使用自已的 fork 仓库。
场景四:禁止被依赖
另一种使用 replace 的场景是你的 module 不希望被直接引用,比如开源软件 kubernetes,在它的 go.mod 中 require 部分有大量的 v0.0.0 依赖,比如:
由于上面的依赖都不存在 v0.0.0 版本,所以其他项目直接依赖 k8s.io/kubernetes 时会因无法找到版本而无法使用。
因为 Kubernetes 不希望作为 module 被直接使用,其他项目可以使用 kubernetes 其他子组件。kubernetes 对外隐藏了依赖版本号,其真实的依赖通过 replace 指定:
go.sum 文件
我们发现,在 go.mod 下还会生成个 go.sum 文件
用途
下载依赖包有可能被恶意篡改,以及缓存在本地的依赖包也有被篡改的可能,单单一个 go.mod 文件并不能保证一致性构建,Go 开发团队在引入 go.mod 的同时也引入了 go.sum 文件,用于记录每个依赖包的哈希值(SHA-256 算法),在 build 时,如果本地的依赖包 hash 值与 go.sum 文件中记录得不一致,则会拒绝 build。
示例:格式[/go.mod]
正常情况下,每个依赖包版本会包含两条记录,第一条记录为该依赖包版本整体(所有文件)的哈希值,第二条记录仅表示该依赖包版本中 go.mod 文件的哈希值,如果该依赖包版本没有 go.mod 文件,则只有第一条记录
go.sum 生成过程
在项目的根目录中执行 go get 命令的话,go get 会同步更新 go.mod 和 go.sum 文件,go.mod 中记录的是依赖名及其版本,如:
go.sum 文件中则会记录依赖包的哈希值(同时还有依赖包中 go.mod 的哈希值)
在更新 go.sum 之前,为了确保下载的依赖包是真实可靠的,go 命令在下载完依赖包后还会查询 GOSUMDB 环境变量所指示的服务器,以得到一个权威的依赖包版本哈希值。如果 go 命令计算出的依赖包版本哈希值与 GOSUMDB 服务器给出的哈希值不一致,go 命令将拒绝向下执行,也不会更新 go.sum 文件。
go mod tidy
拉取缺少的模块,移除不用的模块
go mod vendor
将 GOPATH/src/pkg/mod 中的缓存包,复制到项目的 vendor 目录中,即使用每个项目使用自身包的模式,类似之前的 govendor 管理方式
尽管如此,默认情况下,go build 将忽略该目录的内容。如果想从 vendor/ 目录中建立依赖关系,需要如下参数 build
go mod download
下载依赖包
go mod edit
编辑 go.mod 文件一般直接用 ide 编辑就行
go mod graph
打印模块依赖图
go mod verify
验证依赖是否正确
go mod why
解释为什么需要依赖
补充与说明
关于自己发布包
go get 命令使用的版本是 git 中的 tag,我们制作包的时候也应该创建合适的 tag,例如
此时一个最佳实践是同时创建新分支 v1,以便后面我们修复错误和更新:
现在,我们可以使用 master 而不必担心破坏我们的版本。
接下来在 v1 分支中做修改,并将其标记为新版本。
始终要保持在 v1 分支中修改,这个分支是始终的进行兼容更新的,如果有大的不兼容的改版,要新建并切换分支 v2,发布新的路径。
模块的使用与更新
我们可以在写代码的时候先写好导入的包等,之后初始化 go mod 模式,go build 或者 go mod tidy 等命令,会自动下载相关导入
小版本更新
go get -u 会更新最新的小版本,举个例子,它会将 1.0.0 更新为 1.0.1 或者 1.1.0 这样类似的版本。go get -u=patch 来获取最新的 patch 更新。 举个例子,它会更新到 1.0.1 而不是 1.1.0 版本。
例子总结:
由于我们的程序使用的是软件包的 1.0.0 版,并且我们刚刚创建了 1.0.1 版,以下任何 命令都会将我们更新到 1.0.1 版:
大版本更新
根据语义版本语义,大版本不同于小版本。大版本会破坏向后兼容性。从 Go 模块的角度来看,大版本是一个完全不同的包。一个库的两个不兼容的版本,实质上是两个不同的库。其余的和以前一样,我们推它,把它标记为 v2.0.0 (并可选地创建一个 v2 分支。)
评论 3 条评论