gomod版本表达
版本
module 有版本,Go里遵守 Semantic Versioning 2.0.0,因此,版本基本都会长这样: v0.0.1, v1.2.3, v2.0.0 等等。通常从git仓库中读取的时候,就是以tag为准,有的仓库没有打tag,那么Go会自动生成一个 版本号,称之为 pseudo-version,也就是伪版本号,例如 v0.0.0-20191109021931-daa7c04131f5。大概的格式是:
- vX.0.0 基础版本
- 时间戳,格式为 yyyymmddhhmmss,时间值是该次提交的UTC时间
- 提交哈希值的前12位
上面三个,用 - 连起来。 也叫做伪版本号。
比如v2.3.4
Go使用semantic version来标识package的版本。具体来说:
- MAJOR version when you make incompatible API changes(不兼容的修改)
- MINOR version when you add functionality in a backwards compatible manner(特性添加,版本兼容)
- PATCH version when you make backwards compatible bug fixes(bug修复,版本兼容)

这里,只要模块的主版本号(MAJOR)不变,次版本号(MINOR)以及修订号(PATCH)的变更都不会引起破坏性的变更(breaking change)。这就要求开发人员尽可能按照semantic version发布和管理模块(实际是否遵守以及遵守的程度不能保证,参考Hyrum’s Law)
大版本
也许我们的程序,会出现 2.0, 3.0, 4.0,那么,Go提供的方案是什么呢?答案是,建一个子目录,例如 v2 版本,就建立一个 v2 子目录,v3 就建 v3 子目录。或者如果在顶级目录的话,就要在 go.mod 声明的路径里最后加一个 v2 或者 v3 的后缀。
说实话,不是很优雅。他这个决定,主要是基于这么一条准则:
If an old package and a new package have the same import path, the new package
must be backwards compatible with the old package.
也就是说,同一个大版本号内的代码,必须是兼容的。Go这样做,带来的一个问题是,例如从 v2 升级到 v3 的时候,也许 的确有一些不兼容的地方,但是大部分代码都还是兼容的,又不是完全重写。而这样做了以后呢,调用者原本是导入 github.com/x/aaa,现在要全部改成导入 github.com/x/aaa/v2,并且以前所有引用到的地方都需要改。此外 v2 里的aaa.XXXStruct 和 v1 版本里的 aaa.XXXStruct 是不兼容的,无法互相赋值的,这样就会导致调用者需要改很多东西,带来不少负担。
go.mod 文件
上面我们说了,有 go.mod 文件的地方,就是一个module。我们来看看 go.mod 的格式,先来看一个例子:
module example.com/my/thing
go 1.12
require example.com/other/thing v1.0.2
require example.com/new/thing/v2 v2.3.4
exclude example.com/old/thing v1.2.3
replace example.com/bad/thing v1.4.5 => example.com/good/thing v1.4.5
retract [v1.9.0, v1.9.5]
module声明模块自身的路径go声明这个模块所用的Go版本。 最低 Go 版本, 编译此模块中包所需的最低 Go 版本。require声明这个模块依赖的模块exclude让go mod不要加载这个模块replace声明替换,=>前是代码中的路径,后是要替换成的路径retract声明这个模块中,损坏的版本,或者要撤回,要大家别用的版本。作用是,设置了的版本,使用者碰到这个 版本的时候,就会看到对应的提示
里面所有的路径,都是 URL 加 空格,加 版本号的格式,例如 example.com/new/thing/v2 v2.3.4。
如果一个已解析的包并没有被主module直接导入,则需要添加注释 // indirect
- 有两种情况会发生这种情况:
- 直接依赖A未启用go module,那么A依赖的BCD就会在主moudle中出现,并添加 // indirect
- 直接依赖A启用了go module,但A依赖的D未添加到A的依赖中,也需要在主module中依赖D,并添加 // indirect
- 当所有go项目都迁移到go module后,就不会出现间接依赖的注释了
indirect:
- 没使用gomod
- 不在gomod里
- 手动指定较新版本
子目录
有的时候,我们想要给子目录打一个版本,那怎么做呢?答案是在版本号前面加上目录的路径就可以,例如:
module/
- A
- A/B
那我们执行 git tag A/B/v0.1.2 就可以。这样使用者在使用该包时,就会优先选择子目录的版本,而不是根目录的版本号。
命令行 及 实践
命令
我们来看看常见的和版本控制有关系的命令:
go mod init github.com/xxx/yyy 声明一个模块
go mod tidy 执行模块选择,并且把选择的依赖写到 go.sum 文件里。 添加缺失并去除无用的模块 会移除 go.mod/go.sum 中未被引用的模块。
go mod why 解释引入某个依赖的依赖链,解释为什么包和模块是需要的
go build 构建二进制
go get 添加依赖
go get -u 更新依赖
go mod xxx.
download 下载modules到本地cache
edit 使用工具或脚本编辑go.mod文件
graph 打印现有的模块依赖图
vendor 把当前依赖复制出一个vendor包
verify 校验依赖是预期的内容
go get xxx@P, P = latest : 升级为最新的语义化版本
go mod why -m 依赖 可查找依赖链
go mod why -m all 可查找所有依赖链
- 命令go get只会拉取指定依赖的版本,如果没有指定版本就会拉取最新版本,且不会自动添加间接依赖的版本。
- 命令go get -u除了拉取指定依赖版本外,还会拉取并添加间接依赖的最新版本,而不是直接依赖所依赖的版本,也不会回退间接依赖的版本。

查看有更新的直接依赖项的方法
go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}{{end}}' -m all
该方法还有变种,比如查看更新的版本信息:
go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: {{.Version}} -> {{.Update.Version}}{{end}}' -m all
更新他们:
go get -u $(go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}{{end}}' -m all)
或者
go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}{{end}}' -m all | xargs go get -u
交互式更新:
go get -u github.com/oligot/go-mod-upgrade
更新指向到某个 commit 或者分支
直接如下方式
go get github.com/example/project@abcdef123456
go get github.com/user/repo@branch_name
你也可以直接在 go.mod 中手动指定依赖为某个 commit:
require github.com/example/project v0.0.0-<时间戳>-<commit哈希>
这个时间戳和哈希是 Go 的伪版本号格式(Go pseudo-version),格式为:
v0.0.0-<yyyymmddhhmmss>-<commit>
你可以运行 go list -m -versions github.com/example/project 或 go list -m github.com/example/project@commit 来生成。
例如:
require github.com/example/project v0.0.0-20240601123456-abcdef123456
使用本地模块替代
replace example.com/mod => ../local/path/to/mod
在
go.mod中写入,可以指向本地目录,便于开发调试。
go.work 工作区
用处: 直接 replace 到本地另一个 repo ,同时不用修改 go.mod 文件,到时 git 不 push go.work 文件即可。
在 workspace 目录中,运行
$ go work init ./hello
go work init 命令告诉 go 为包含 ./hello 目录中模块的工作区创建一个 go.work 文件。
go 命令将生成一个 go.work 文件,如下所示
go 1.18
use (
./hello
./example
)
replace (
github.com/link1st/example => ./example
)
go.work 文件具有与 go.mod 相似的语法。
go 指令告诉 Go 该文件应使用哪个版本的 Go 进行解释。它类似于 go.mod 文件中的 go 指令。
use 指令告诉 Go hello 目录中的模块在进行构建时应为主模块。
use 和 replace都是指定本地项目目录
replace 则表示项目中的 github.com/link1st/example 在本地 ./example1中找
多模块
依赖图形化
todo
版本选择策略Minimal version selection
Minimal version selection也即最小版本选择,如果光看上述的引用可能会很迷惑(或者矛盾):明明是选择最新的版本(keep only the newest version),为什么叫最小版本选择?
Go 选择最低的兼容版本。
对每个依赖项,选择其所有被依赖版本中的最高的那个版本
一句话:从当前项目 main 开始,构建一颗依赖树,当多个子模块依赖同一个模块时,选最新的。
如上图,main 依赖 A1.2, B1.2,A1.2 依赖 C1.3,而 B1.2 依赖 C1.4,他们同时依赖 D1.2。最后,go mod 会选择 main, A1.2, B1.2, C1.4, D1.2。
go mod 设置
GOROOT, GOPATH
Go开发相关的环境变量如下:
- GOROOT:GOROOT就是Go的安装目录,(类似于java的JDK)
- GOPATH:GOPATH是我们的工作空间,保存go项目代码和第三方依赖包
GOPATH可以设置多个,其中,第一个将会是默认的包目录,使用 go get 下载的包都会在第一个path中的src目录下,使用 go install时,在哪个GOPATH中找到了这个包,就会在哪个GOPATH下的bin目录生成可执行文件
GOPATH是开发时的工作目录。用于:
- 保存编译后的二进制文件。
go get和go install命令会下载go代码到GOPATH。- import包时的搜索路径
使用GOPATH时,GO会在以下目录中搜索包:
GOROOT/src:该目录保存了Go标准库代码。GOPATH/src:该目录保存了应用自身的代码和第三方依赖的代码。
GO111MODULE
GO111MODULE 有三个值:off, on和auto(默认值)。
GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。GO111MODULE=auto, 默认值,go命令行将会根据当前目录来决定是否启用module功能。
若出现如下情况,则会使用go mod
- 项目目录不在`go path`下
- 当前或者上一级目录存在`go.mod`文件