gomod版本管理

2022/02/18 Go 共 4943 字,约 15 分钟

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修复,版本兼容)

img

这里,只要模块的主版本号(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:

  1. 没使用gomod
  2. 不在gomod里
  3. 手动指定较新版本

子目录

有的时候,我们想要给子目录打一个版本,那怎么做呢?答案是在版本号前面加上目录的路径就可以,例如:

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 可查找所有依赖链
  1. 命令go get只会拉取指定依赖的版本,如果没有指定版本就会拉取最新版本,且不会自动添加间接依赖的版本。
  2. 命令go get -u除了拉取指定依赖版本外,还会拉取并添加间接依赖的最新版本,而不是直接依赖所依赖的版本,也不会回退间接依赖的版本。

image-20250712084508962

查看有更新的直接依赖项的方法

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 工作区

参考 1

参考 2

用处: 直接 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 开始,构建一颗依赖树,当多个子模块依赖同一个模块时,选最新的。

Go minimal version selection

如上图,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是开发时的工作目录。用于:

  1. 保存编译后的二进制文件。
  2. go get和go install命令会下载go代码到GOPATH。
  3. import包时的搜索路径

使用GOPATH时,GO会在以下目录中搜索包:

  1. GOROOT/src:该目录保存了Go标准库代码。
  2. 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`文件

Search

    Table of Contents