Go-知识依赖管理
在 go.mod 文件中通过指令声明 module 信息,用于控制 Go 命令行工具进行版本选择。module: 声明 module 的名称require: 声明依赖及其版本号replace: 替换 require 中声明的依赖,使用另外的依赖及其版本号exclude: 禁用指定的依赖retract: 标记有问题的版本go 标记当前模块开发所用的 Go 语言版本。module 用于指定 module 的名字,比如, module 的名字直接决定了引用者的 import 路径。_go指定依赖仓库
文章信息
- 原文链接:https://jiayq.blog.csdn.net/article/details/143722028
- 发布时间:2024-11-12 19:28:57
- 阅读量:955
- 分类:Golang专栏收录该内容, 订阅专栏
- 标签:#golang, #go, #Go Module, #go mod, #模块
摘要
文章浏览阅读955次,点赞13次,收藏20次。在 go.mod 文件中通过指令声明 module 信息,用于控制 Go 命令行工具进行版本选择。module: 声明 module 的名称require: 声明依赖及其版本号replace: 替换 require 中声明的依赖,使用另外的依赖及其版本号exclude: 禁用指定的依赖retract: 标记有问题的版本go 标记当前模块开发所用的 Go 语言版本。module 用于指定 module 的名字,比如, module 的名字直接决定了引用者的 import 路径。_go指定依赖仓库
Go-知识依赖管理
- 1. 介绍
- 2. replace
- 2.1 replace 的工作机制
- 2.2 replace 的使用场景
- 2.2.1 替换无法下载的包
- 2.2.2 调试依赖包
- 2.2.3 使用 fork 仓库
- 2.2.4 禁止被依赖
- 2.2.1 替换无法下载的包
- 2.1 replace 的工作机制
- 3. exclude
- 3.1 排除指定版本
- 4. indirect
- 4.1 直接依赖未启用 Go Module
- 4.2 直接依赖的 go.mod 文件不完整
- 4.3 总结
- 4.1 直接依赖未启用 Go Module
- 5. 版本选择机制
- 5.1 依赖包版本约定
- 5.2 版本号约定
- 5.3 版本选择机制
- 5.1 依赖包版本约定
- 6. incompatible
- 6.1 能否引用不兼容的包
- 6.2 如何处理incompatible
- 6.1 能否引用不兼容的包
- 7. 伪版本
- 7.1 什么是伪版本
- 7.2 伪版本风格
- 7.3 如何获取伪版本
- 7.1 什么是伪版本
- 8. 依赖包存储
1. 介绍
在 go.mod 文件中通过指令声明 module 信息,用于控制 Go 命令行工具进行版本选择。 支持的指令包括:
- module: 声明 module 的名称
- require: 声明依赖及其版本号
- replace: 替换 require 中声明的依赖,使用另外的依赖及其版本号
- exclude: 禁用指定的依赖
- retract: 标记有问题的版本
- go 标记当前模块开发所用的 Go 语言版本。
module 用于指定 module 的名字,比如 module github.com/he/helloworld, module 的名字直接决定了引用者的 import 路径。
require 用于指定依赖,如 require github.com/google/uuid v1.6.0, 用于指示参与构建的版本。
replace 用于替换出现在 require 列表中的依赖及其版本。
exclude 用于指定禁用的依赖及其版本,类似黑名单。
retract 用于发布有问题的版本,告知 go 命令行及用户应该避开的版本。
go.sum 中的 go 指明了模块开发所用的版本。
2. replace
2.1 replace 的工作机制
replace 指替换,用于替换 require 指令中出现的包。
1
2
3
4
5
module github.com/he/helloworld
go 1.21.10
require github.com/google/uuid v1.6.0
使用 require 指定了 uuid 使用 v1.6.0 版本,使用 go list -m all 查看最终选定的版本:

如果想使用 v1.5.0 版本,除了直接修改 require 的版本,还可以使用 replace 指定。
1
2
3
4
5
6
7
module github.com/he/helloworld
go 1.21.10
require github.com/google/uuid v1.6.0
replace github.com/google/uuid v1.6.0 => github.com/google/uuid v1.5.0
replace 使用的条件:
- replace 仅在当前 module 有效,比如当前在编译
github.com/he/helloworld,那么就有效。如果其他项目引用了github.com/he/helloworld
那么在其他项目表一的时候,这里的 replace 就会被自动忽略。 replace 中
=>前面的包及其版本号必须在 require 中指定过,否则 replace 无效,会被忽略。
比如:module github.com/he/helloworld
go 1.21.10
require github.com/google/uuid v1.6.0
replace github.com/google/uuid v1.7.0 => github.com/google/uuid v1.5.0
会自动忽略。
2.2 replace 的使用场景
2.2.1 替换无法下载的包
由于一些地区网络的问题,有些包无法顺利下载,比如一些外网的包,如果没有有效的网络,那么下载就会失败。
比如如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"github.com/google/uuid"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func main() {
s := "gopher"
fmt.Println("Hello and welcome, %s!", s)
uuid.New().String()
message.NewPrinter(language.BritishEnglish)
for i := 1; i <= 5; i++ {
fmt.Println("i =", 100/i)
}
}
这里引入了两个 golang 下面的包 golang.org/x/text/language 和 golang.org/x/text/message 。
执行 go get 或者 go mod tidy 后,会分析以来情况,并更新 go.mod 文件
1
2
3
4
5
6
7
8
9
10
module github.com/he/helloworld
go 1.21.10
require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.20.0
)
replace github.com/google/uuid v1.7.0 => github.com/google/uuid v1.5.0
依赖golang.org/x/text v0.20.0 被添加到了 require 指令中(多个require会自动合并)。
实际上在没有指定golang.org/x/text的版本号的时候,Go 命令行工具根据默认的版本计算规则,使用 v0.20.0 版本。
如果无法直接下载 golang.org/x/text ,但是可以访问 github 或者国内的镜像库,就可以使用 replace 来使用 github 或者别的镜像上的依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
module github.com/he/helloworld
go 1.21.10
require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.20.0
)
replace (
github.com/google/uuid v1.7.0 => github.com/google/uuid v1.5.0
golang.org/x/text v0.20.0 => github.com/golang.org/x/text v0.20.0
)
这个时候,项目编译就会从 github 上下载依赖包,而源码中的 import 路径还是 golang.org/x/text/xxxx ,不需要做任何改变。
是否可以将 import 路径 从
golang.org/x/text/xxxx改为github.com/golang/x/text/xxxx,这样就不用replace 了?
不行。 因为github.com/golang/x/text/xxxx只是镜像仓库,其 go.mod 文件中定义的 module 还是module golang.org/x/text/xxxx。
2.2.2 调试依赖包
如果项目是对外提供依赖包,那么在对外提供之前,可能需要调试依赖包,模拟其他用户引入该依赖包是否工作正常。
1
2
3
4
5
6
7
8
9
10
module github.com/he/helloworld
go 1.21.10
require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.20.0
)
replace github.com/google/uuid v1.7.0 => ./uuid
这样就能进行调试了,需要注意 uuid 目录下应该是一个完整的项目, 需要有 go.mod 并且 module 的 name 和依赖的 name 完全相同。
2.2.3 使用 fork 仓库
有时在使用开源的依赖包时发现了 Bug ,在开源版本还未修改或者没有新的版本发布时,可以使用 fork 仓库,在 fork 仓库中进行修复。
接着在 fork 仓库发布新的版本,并相应的修改 go.mod 来使用 fork 仓库。
fork 仓库其实很类似于 2.2.2 中的调试本地代码一样,只不过 fork 仓库实在远端,而调试实在本地。
使用 fork 仓库只是一种临时性的做法,开源仓库一旦修复,需要尽快切换到开源版本。
2.2.4 禁止被依赖
如果 module 不希望被直接引用,比如开源项目 kubernetes ,在它的 go.mod 中 require 部分有大量的 v0.0.0 依赖

由于依赖都不存在 0.0.0 版本,所以如果其他项目项目直接依赖 k8s.io/kubernetes 时会因为无法找到版本而无法使用。kubernetes 不希望作为一个整体的 module 被直接使用,
其他项目必须引用 kubernetes 的子module.
replace 指令在当前模块不是编译 module 时,会自动被忽略, kubernetes 就是利用这一个特点,对外隐藏了依赖的版本号,这样别的项目
直接依赖了 kubernetes ,也会因为依赖不全而无法编译使用。最终达到了禁止直接引用的目的。
3. exclude
go.mod 文件中的 exclude 指令用于排除某个包的特定版本,与 replace 类似,也是仅在编译当前 module 的时候生效。在被引用的时候,exclude 会被忽略。
exclude 指令在实际的项目中很少被使用,很少会显式的排除某个包的某个版本,除非已知了某个版本有非常严重的bug。
3.1 排除指定版本
比如在之前的项目中,Go 命令行自动计算出 golang.org/x/text 依赖的版本号是 v0.20.0 版本,如果不想使用这个版本,使用 exclude 进行排除:
1
2
3
4
5
6
7
8
9
10
module github.com/he/helloworld
go 1.21.10
require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.20.0
)
exclude golang.org/x/text v0.20.0
发现 Go 命令行进行了版本回退,并重新下载了依赖。
4. indirect
go.mod 文件中的 // indirect 表示总是出现在 require 指令中,其中 // 与代码的行注释样表示注释的开始, indirect表示这个包
并没有被当前 module 直接依赖,可能是间接依赖。
在执行 go mod tidy 命令时, Go Module 会自动整理 go.mod 文件,如果有必要,则会在部分依赖包的后面增加 // indirect 注释。 被添加 indirect 注释的依赖包说明该依赖包被间接引用,
而没有添加// indirect注释的依赖包则是被直接引用的,即明确的出现在某个 import 语句中。
go.mod 文件中出现间接依赖还有可能是 1. 直接依赖未启用 Go Module ; 2. 直接依赖的 go.mod 文件中缺失部分依赖。
4.1 直接依赖未启用 Go Module
Module A 依赖 Module B,但是 B 还未切换成 Module ,没有 go.mod 文件,当使用 go mod tidy 命令更新 A 的 go.mod 文件时,B 的两个依赖 B1 和 B2 会被添加到
A 的 go.mod 文件中(前提是 A 之前没有依赖 B1 和 B2),并且 B1 和 B2 还会被添加 // indirect 的注释。

Module A 的 go.mod 文件中的 require 部分将变成:
1
2
3
4
5
require (
B vx.x.x
B1 vx.x.x // indirect
B2 vx.x.x // indirect
)
依赖 B 和 B 的依赖 B1 和 B2 都会出现在 go.mod 中。
4.2 直接依赖的 go.mod 文件不完整
在 4.1 的例子中,如果依赖 B 没有 go.mod 文件,则 Module A 会把 B 的所有依赖记录到 A 的 go.mod 文件中。
即使B有 go.mod 文件,如果 go.mod 文件不完整,那么 Module A 依然会记录部分 B 的依赖到 go.mod 文件中。

Module B 虽然提供了 go.mod 文件,但 go.mod 文件中只添加了依赖 B1 ,当 A 引用 B 时,会在 A 的 go.mod 文件中
添加 B2 作为间接依赖, B1 则不会出现在 A 的 go.mod 中。
Module A 的 go.mod 文件中 require 将是这样的:
1
2
3
4
require(
B vx.x.x
B2 vx.x.x // indirect
)
由于 B1 已经包含在 B 的 go.mod 文件中,因此 A 的 go.mod 文件不必再记录,只会记录缺失的 B2。
4.3 总结
为什么要记录间接依赖
如果某个依赖 B 没有 go.mod 文件,在 A 的 go.mod 文件中已经记录了依赖 B 及其版本号,为何还要增加间接依赖?
Go Module 需要精确地记录软件的依赖情况,虽然记录了依赖 B 的版本号,但是 B 的依赖情况没有记录下来,所以如果 B 的
go.mod 文件缺失或者没有这个信息,则需要在 A 的 go.mod 文件中记录下来。此时间接依赖将根据 Go Module 的版本选择机制确定一个最优版本。
如何处理间接依赖
间接依赖不需要刻意清理。
间接依赖出现在 go.mod 文件中,可能表示其不支持 Go Module ,也可能是其 go.mod 文件不完整,随着时间的推移,
越来越多的项目会使用 Go Module 来管理依赖,升级新的依赖版本即可。
如何查找间接依赖来源
Go Module 提供了 go mod why 命令来解释为什么会依赖某个软件包,若要查看 go.mod 中某个间接依赖是被那个依赖引入的,
可以使用 go mod why -m <pkg>命令。

比如想知道github.com/jinzhu/now v1.1.5 // indirect 间接依赖是如何引入的,可以使用go mod why -m github.com/jinzhu/now查询

# github.com/jinzhu/now 表示当前正在分析的依赖,后面的则表示依赖链。
go mod why -m all命令可以分析所有依赖的依赖链。
5. 版本选择机制
在使用go get <pkg>获取某个依赖,如果没有特别指定依赖的版本号,那么 go get 会自动选择一个最优版本,并且如果本地有 go.mod 文件,还会自动更新 go.mod 文件。
除了 go get,go build和go mod tidy也会自动选择依赖的版本。
5.1 依赖包版本约定
Go 语言提供了一个规范,并且 Go 语言的严谨过程中也一直遵循这个规范。
Go Module之前版本的兼容性
在 Go 1.11(开始引入 Go Module的版本)之前,Go 官方就建议依赖包需要保持向后兼容,这包括可导出的函数,变量,类型,常量等不可以随便删除。以函数为例,如果需要修改函数的入参,那么可以增加新的函数而不是直接修改原有的函数。
如果确实需要做一些打破兼容性的修改,则建议创建新的包。
比如原来的依赖是 import github.com/some/xxx/A
引入一个不兼容的特性,在原来的仓库中新增一个新的 package A1,此时仓库中包含两个包:
github.com/some/xxx/A
github.com/some/xxx/A1
其他项目在升级依赖包版本后不需要修改原有的代码就可以继续使用 package A ,如果需要使用新的 package A1 ,那么只需要将 import 的路劲修改为 A1并做相应的适配即可。
Go Module之后版本的兼容性
从 Go 1.11 开始,随着 Go Module 特性的引入,依赖包的兼容性要求有了进一步的延伸,Go Module开始关心依赖包版本管理系统(如Git)中的版本号,
不过核心内容没变:
- 如果新的 package 和旧的 package 拥有相同的 import 路劲,那么新的 package 必须兼容旧的 package。
- 如果新的 package 不能兼容旧的 package ,那么新的 package 需要更换 import 路径。
5.2 版本号约定
在 Go Module 时代,module版本号需要遵循语义化版本规则,即版本号格式为 v<major>.<minor>.<patch>,如果有不兼容的改变时,需要增加 major版本号。
Go Module 规定,如果 major 的版本号大于1,则 major 的版本号需要显式的标记在 module 名字中。
比如 github.com/some/mod/v2 这样做的好处是 , Go Module 会把 module github.com/some/mod和module github.com/some/mod/v2
当做两个 module ,甚至可以被同时引用。
如果 module 的版本号为
v0.x.x或者v1.x.x不需要在 module 中体现版本号。
5.3 版本选择机制
Go 的多个命令行工具都具有自动选择依赖版本的能力,如 go build 和 go test ,当在源码中增加了新的 import 后,这些命令将会自动选择一个最优的版本,并更新 go.mod 文件。
如果 go.mod 中已经标记了某个依赖包的版本号,则这些命令不会主动更新 go.mod 中的版本号。所谓自动更新版本号只在 go.mod 中缺失某些依赖或者依赖不匹配时才会发生。
最新版本选择
挡在源码中新增了一个import 后,如果 go.mod 的 require 指令中并没有包含这个依赖,那么 go build 或 go test 命令则会去仓库中寻找最新的符合语义化版本规范的版本,比如 v1.2.3,
并在 go.mod 文件中增加一条 require 依赖require xxx v1.2.3
由于 import 路径中没有类似于 v2 或更高的版本号,所以版本选择时,只会选择 v1.x.x,不会选择 v2.x.x 或更高的版本。
最小版本选择
有时记录在 go.mod 中的依赖包版本会随着引入其他依赖包而发生变化

Module A 依赖 Module M 的 v1.0.0 ,但之后 Module A 引入了 Module D,而 Module D 依赖 Module M 的 v1.1.1,此时,由于依赖的传递, Module A 也会选择 v1.1.1
此时会自动选择最小可用的版本,而不是最新的tag.
总结
Go 语言针对依赖包版本管理的约定,这个不算强制性的要求,但如果不遵守该约定,那么后续该依赖包的使用者将遇到各种麻烦,最终有可能会启用这个不规范的依赖包。
Go Module 机制使用了自动版本选择算法,除了自动版本选择,还以显式的指定依赖包的版本。另外,除了在 go.mod 文件中指定依赖的 tag 版本号,还可以使用假的版本号。
6. incompatible
Go Module 的版本选择机制,其中m module 的版本号要遵循 v<major>.<minor>.<patch> 的格式,并且,如果major的版本号大于1,那么其版本号还需要体现在 module 名字中。
如果 module 的 major 的版本号变成了 v2.x.x,但是 module 名字仍保持原样会如何?
6.1 能否引用不兼容的包
module 名字未遵循 Go 所推荐的风格,即 module 名字中未附带版本信息,这种module就是不规范的 module.
不规范的 module 还是可以引用,但是与引用规范的 module 有不同之处。
如果在项目A中引用了不符合规范的module,在使用go mod tidy命令自动查找module的最新版本时,因为是不规范的 module ,为了加以区分,go 命令会在 go.mod 中增加 +incompatible标识
require github.com/some/xx v1.0.0+incompatible
对于使用不规范模块的项目来说,在不规范模块后面增加+incompatible标识并不影响使用。
从 Go 1.14 起,如果不规范模块中也包含 go.mod 文件(原来不支持 Go Module ,后面支持了,但是可能仍然不规范,比如 v2.x.x 但是依然没有在 module name 中体现 v2)
其他项目在使用这个不规范的模块时,go get将不会自动选择不兼容的版本(不会选择 v2.x.x 版本)。
6.2 如何处理incompatible
go.mod 文件中出现 +incompatible ,说明引用了一个不规范的 module,正常情况下,只能说明这个 module 版本未遵循版本语义化规范,但是引用这个不规范的 module 也存在风险。
以开源 github.com/blang/semver为例,最新版本为v4.x.x,但是在 go.mod 中确是:

还是 module github.com/blang/semver
如果依赖 github.com/blang/semver ,那么就会标记为 +incompatible

也符合了自动选择版本时最小版本原则。
但是选择不规范模块,存在一定的困扰,现在 github.com/blang/semver 发布了新版本 v4.x.x,并且在仓库中新建了 v4 的 package

而且 v4 版本也完全符合语义化规范,但是对于使用者来说,从 v3 升级到 v4 ,是出现了不兼容的升级,那么直接升级就会导致程序执行失败。
而 go 命令行也不会自动选择 v4 版本,导致升级的意愿降低。
最终就是会导致很多项目会放弃 github.com/blang/semver,转而使用更规范的 module 替代,该开源项目也面临无人使用的窘境。(事实上也确实如此,该项目在4年前基本上就不在更新了)
7. 伪版本
在 go.mod 中通常使用语义化版本来标记依赖,比如 v1.2.3,v0.1.5 等等。因为 go.mod 文件通常是 go 命令自动生成并修改的,所以 go 命令比较倾向于实用语义化版本。
而 v1.2.3 这样的语义化版本,实际上是某个 commit ID 的标记,真正的版本还是 commit ID。(v1.2.3 实际就是打了个 tag)

由于语义化版本比 commit ID 更加直观,所以更多的使用 语义化版本。
7.1 什么是伪版本
在实际项目中,有时不得不直接使用一个 commit ID,比如某个项目临时发了bugfix,但是还未来得及发布新的版本,如果急需使用该依赖,那么可以直接引用 commit ID,等新的版本发布后再替换为语义化版本(一般可能在多人协作开发中用的比较多).
伪版本的版本号通常会使用 vx.y.z-yyyymmddhhmmss-abcdefabcdef格式,其中 vx.y.z看上去像是一个真实的语义化版本,但是实际上可能并不存在,所以被称为伪版本。
abcdefabcdef表示某个 commit ID 的前 12 位,而 yyyymmddhhmmss表示 commit 的提交时间,方便比较。
比如 replace github.com/juju/errors => github.com/juju/errors v0.0.0-20220331221717-b38fca44723b
因为使用 github.com/juju/errors 的自动选择版本存在问题,那么就先使用某个没有问题的伪版本。
7.2 伪版本风格
伪版本的格式都为 vx.y.z-yyyymmddhhmmss-abcdefabcdef ,但是 vx.y.z部分在不同的情况下存在区别,有时是vx.y.z-pre.0
甚至vx.y.z-dev.2.0等等。
vx.y.z的具体格式取决于所引用 commit ID 之前的版本号,如果所引用 commit ID 之前最新的 tag 版本为 v1.6.0 。位版本号则在其基础上增加一个标记,比如 v1.6.0-0,看上去像是下一个版本一样。
实际使用中,go 命令会帮我们自动生成伪版本,不需要手动计算。
7.3 如何获取伪版本
假设存在一个 commit ID,并且该 commit ID 没有发布新的版本。

如果想依赖 commit ID=9ed46a7c4e05a58d4080fe59bf3300f034758ee2 的伪版本,那么可以使用
go get github.com/jinzhu/now@9ed46a7c4e05a58d4080fe59bf3300f034758ee2
进行更新(可以使用完整的 commit ID,也可以使用前 12 位)

在 go.mod 中也会自动更新为 github.com/jinzhu/now v1.1.5-0.20220317042121-9ed46a7c4e05 // indirect
在之前的 tag v1.1.5 后面加了 -0
8. 依赖包存储
go get 命令会将依赖包下载到 $GOPATH/pkg/mod 目录下,并且按照依赖包的版本分别存放(当 go get 命令不指定特定版本时,默认会下载最新版本)

由于依赖包的每个版本都有一个唯一的目录,所以在多项目场景中需要使用同一个依赖包的多版本时才不会产生冲突。
另外由于依赖包的每个版本都有唯一的目录,所以该目录内的内容也不会发生改变,也就不需要再存储其位于版本管理系统(Git)中的版本历史信息。
只需要下载模块的代码文件,而不必克隆整个仓库。
包名大小写敏感问题
有时依赖的包名会包含大写字符,在 Go Module 中,存储依赖包时,会将包名做大小写编码处理:每个大写字母将变成!+小写字母。

在 Go Module 下,使用 go get 命令时,如果不小心将某个包名大小写写错,比如
github.com/google/uuid写成github.com/google/UUID
那么在存储依赖包时,会严格按照 go get 的指示进行存储。
比如使用go get github.com/google/UUID
该问题已经被修复了
之前因为域名不区分大小写,所以使用
github.com/google/UUID也能下载依赖,并且会存储两份。








