模块、包和版本
模块由一系列已发布、版本化、被分发的包构成。可以直接从版本控制仓库或模块代理服务器下载模块。
模块由 模块路径 标识,该路径在 go.mod
文件 中声明,并与模块依赖信息放在一起。模块根目录是指包含 go.mod
文件的目录。主模块是指包含了 go
命令的目录对应的模块。
模块下的每个包都是一系列同目录下、将被编译到一起的文件集合。包路径是模块路径和包含包的子目录(相对于模块根目录的路径)拼起来的结果。比如,模块 "golang.org/x/net"
包含了目录 html
下的包。则这个包路径就是 "golang.org/x/net/html"
。
模块路径#
模块路径就是模块的规范名称,被 go.mod
file 中的 module
指令 所声明。
模块路径应该描述模块做什么以及在哪里找到它。通常,模块路径由存储库根路径、存储库中的目录(通常为空)和主要版本后缀(仅适用于主要版本为 2 或更高)组成。
- 存储库根路径是模块路径的一部分,对应开发模块的版本控制仓库的根目录。大多数模块都被定义在存储库根目录,因此这通常就是整个模块路径了。比如,
golang.org/x/net
就是同名模块的存储库根路径。如欲了解更多go
命令如何通过模块路径并使用 HTTP 请求定位仓库的信息,请参阅 寻找模块存储库。 - 如果模块并没有定义于仓库根目录,则模块子目录是命名目录的模块路径的一部分,且不包括主版本后缀。这一规则也作用于语义化版本标签的前缀。比如,
golang.org/x/tools/gopls
表示模块在存储库根路径golang.org/x/tools
的子目录gopls
下,因此它具有模块子目录gopls
。参见 版本映射至提交 以及 同一仓库下的模块名称。 - 假设模块发布在版本 2 或更高,模块路径必须有像
/v2
这样的 主版本后缀。例如,路径为golang.org/x/repo/sub/v2
的模块可以位于存储库golang.org/x/repo
的/sub
或/sub/v2
子目录中。
如果某个模块需要被其他模块所依赖,则必须遵循这些规则,以便 go
命令可以查找和下载该模块。模块路径中允许的字符也有一些 语法限制。
版本#
一个版本标示着模块的不可变快照,可以是 正式发行版本 或 pre-release 版本。每个版本都以字母 v
开头,跟着语义版本。有关语义版本的格式化、解释和比较的详细信息,请参见 语义版本 2.0.0。
总而言之,一个语义版本由三个非负整数(主要、次要和补丁版本,从左到右)用点分隔组成。补丁版本后面可以跟一个以连字符开头的标识符(例如 -pre
或 -beta
)。预发发行版或补丁版本后面可以跟以加号开头的构建元数据(build metadata)字符串。例如, v0.0.0
、v1.12.134
、v8.0.5-pre
和 v2.0.9+meta
都是有效版本。
版本的每个部分都表示该版本是否稳定,是否与之前的版本兼容。
- 主要版本号 在发布不兼容的公共接口更改后,例如模块里的某个包被删除,必须递增,必须将次要和补丁版本设置为零。
- 次要版本号 在发布向后兼容的更改后,例如添加新函数后,必须递增且补丁版本设置为零。
- 补丁版本号 在不修改到公共接口的情况下,例如 Bug 修复或者做了一些优化,必须递增。
-pre
后缀意味着某个版本的预发版,在指定版本的后面添加。例如在v1.2.3
之前,发布v1.2.3-pre
预发版。- 构建元数据会在版本比较时被忽略。附带构建元数据的标识符会在版本库中被忽略,然而在
go.mod
文件中会被保留。后缀+incompatible
表示在迁移到模块版本主版本 2 或更高版本之前发布的版本。(详见 兼容非模块的代码库 )。
如果一个版本的主要版本是 0 或者它有一个预发布后缀,那么它就被认为是不稳定的。 不稳定的版本不受兼容性要求的限制。 例如,v0.2.0
可能与 v0.1.0
不兼容,v1.5.0-beta
可能与 v1.5.0
不兼容。
Go 可能会使用不遵循这些约定的标签、分支或修订来访问版本控制系统中的模块。 但是,在主模块中,go
命令会自动将不符合此标准的修订名称转换为规范版本。 作为此过程的一部分,go
命令还将删除构建元数据后缀(+incompatible
除外)。 这可能会导致一个 pseudo-version,一个编码修订标识符(例如 Git 提交哈希)的预发布版本和一个 来自版本控制系统的时间戳。 例如,命令 go get -d golang.org/x/net@daa7c041
会将提交哈希 daa7c041
转换为伪版本 v0.0.0-20191109021931-daa7c04131f5
。 主模块之外需要规范版本,如果 go.mod
文件中出现像 master
这样的非规范版本,go
命令会报错。
伪版本#
伪版本是一种特殊格式的 预发布 版本 对版本控制存储库中特定修订的信息进行编码。 例如,v0.0.0-20191109021931-daa7c04131f5
是一个伪版本。
伪版本可能是指没有 语义版本标签 可用的修订。 它们可用于在创建版本标签之前测试提交,例如在开发分支上。
每个伪版本有三个部分:
- 基础版本前缀 (
vX.0.0
orvX.Y.Z-0
), 它派生自修订之前的语义版本标记,如果没有这样的标记,则派生为vX.0.0
. - 时间戳 (
yyyymmddhhmmss
), 即创建修订的 UTC 时间。在 Git 中,这是提交时间,而不是作者时间. - 修订版标识符 (
abcdefabcdef
), 它是提交散列的 12 个字符的前缀,或者在 Subversion 中,用零填充的修订号.
每个伪版本可以是三种形式中的一种,具体取决于基础版本。这些形式确保伪版本比其基础版本高,但低于下一个标记版本.
vX.0.0-yyyymmddhhmmss-abcdefabcdef
当没有已知的基础版本时使用。与所有版本一样,主要版本X
必须匹配模块的 主要版本后缀.vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
当基础版本是vX.Y.Z-pre
等预发行版本时使用.vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
当基础版本是一个像vX.Y.Z
这样的发布版本时使用。例如,如果基本版本是v1.2.3
, 则伪版本可能是v1.2.4-0.20191109021931-daa7c04131f5
.
多个伪版本可以通过使用不同的基础版本来指代相同的提交。当写入伪版本后,当较低版本被标记时,这会自然发生.
这些表单为伪版本提供了两个有用的属性:
- 具有已知基本版本的伪版本排序高于那些版本,但低于后续版本的其他预发布版本.
- 具有相同基本版本前缀的伪版本按时间顺序排序.
go
命令会执行若干个限制检查,以此确保模块作者能控制伪版本与其他版本进行区分,且伪版本引用的修订实际上是模块提交历史记录的一部分。
- 如果指定了基础版本,那就必须有一个对应的语义版本标签,作为伪版本描述修订的起源。这可以防止开发人员绕过最小版本选择的限制,而使用比所有版本标签更高的伪版本进行对比。
- 时间戳必须与修订的时间戳匹配。这可以防止攻击者滥用模块代理,从而使用无限个相同的伪版本。这个限制同时可以防范模块使用者更改版本的相对顺序。
- 修订必须源自模块的仓库分支或者标签。这可以防止攻击者使用未经批准的更改或拉取请求。
伪版本不需要手动输入。有许多命令接受提交的哈希记录或者是分支名,将其自动转换为伪版本(如果可用,可以是标签版本)。比如下面的命令:
go get -d example.com/mod@master
go list -m -json example.com/mod@abcd1234
主版本后缀#
从第 2 个版本开始,模块路径必须有个类似 /v2
的后缀,以此来匹配主版本。例如:如果一个模块在 1.0.0
版本的路径为 example.com/mod
,那么这个模块需要在 v2.0.0
版本时的路径为 example.com/mod/v2
。
主版本后缀实现导入兼容规则:
如果旧包和新包具有相同的路径,那么这个新包需要对旧包向后兼容。
根据定义,模块在新主版本的包与旧主版本中的包不向后兼容。因此,从 v2
版本开始,包必须使用新的导入路径。这是通过模块路径添加一个主版本后缀实现的。由于模块路径是模块内每个包的导入路径的前缀,因此向模块路径添加主版本后缀可为每个不兼容版本,提供不同的导入路径。
主版本后缀不允许在 v0
或 v1
出现。因为 v0
版本不稳定且没有兼容性保证,因此无需更改 v0
和 v1
的模块路径。此外,对于大多数模块,v1
版本与最后一个 v0
版本向后兼容;与 v0
相比, v1
版本视为兼容性的承诺,而不是一个解除非兼容性的标志。
作为特殊情况,即使在 v0
和 v1
,以 gopkg.in/
开头的模块路径必须始终具有主版本后缀。后缀必须以点开头,而不是斜杠(例如,gopkg.in/yaml.v2
)。
主版本后缀允许模块的多个主版本共存于同一版本。由于钻石依赖性问题的存在,这一特性是必要的。通常情况下,如果一个模块需要两个不同版本的可传递依赖项,那么应当使用模块的更高版本。但是,如果这两个版本不兼容,那么这两个版本都不能满足所有客户端的需求。由于不兼容版本号之间必须具有不同的主版本数字,所以它们必须由主版本后缀决定不同的模块路径。这样解决了冲突:具有不同后缀的模块视为一个独立的模块,它们的包 – 甚至是相对于其模块根目录,在同一子目录下的包 – 其模块路径定义也将是不同的。
在迁移到模块之前,许多 Go 项目以 v2
或更高版本号来发布版本,而非使用主要的版本后缀 (可能在引入模块之前就没使用)。这些版本带有 +incompatible
标签注释(例如 v2.0.0+incompatible
)。有关详细信息,请参阅非模块存储库的兼容性。
将包解析为模块#
当 go
命令使用包路径加载包时,它需要确定哪个模块提供包。
go
命令从搜索构建列表开始,把具有包路径前缀的模块。例如,导入了包 example.com/a/b
,并且模块 example.com/a
在构建列表中,那么 go
命令将检查 example.com/a
目录中是否包含需要导入的包。在 b
目录中必须至少存在一个扩展名为 .go
的文件,才能将其视为包。构建约束不适用于此目的。如果构建列表中只有一个模块提供包,则使用该模块。如果没有模块提供包,或者有多个模块提供包,则 go
命令会报错。-mod=mod
标志表示 go
命令尝试查找缺失的包模块,并更新 go.mod
和 go.sum
。go get
命令和 go mod tidy
命令会自动触发此操作。
当 go
命令查找包路径的新模块时,它会检查 GOPROXY
环境变量,这是一个以逗号分隔的代理 URL,或者用关键字 direct
或 off
表示的环境变量。代理 URL 表示的是,go
命令使用模块代理声明到的 GOPROXY
协议。direct
关键字表示的是,go
命令必须与版本控制系统通信。off
关键字表示的是,不应尝试任何通信。GOPRIVATE
和 GONOPROXY
环境变量同样可用于控制 go
命令做这些的行为。
对于 GOPROXY
列表中的每个条目,go
命令会请求每个模块的最新版本可能提供的包(也就是每个包前缀)。请求成功的每个模块路径,go
命令会下载它的最新版本,并且会检查模块是否含有请求的包。如果一个或多个模块含有请求的包,会使用拥有最长路径的模块下的包。如果请求到一个或多个模块后发现,没有一个模块含有请求的包,会报告错误。如果模块没有被请求到,go
命令会尝试下一个 GOPROXY
列表的条目。如果没有剩余条目,则报告错误。
例如,假设 go
命令在寻找提供包的模块名为 golang.org/x/net/html
,并且 GOPROXY
设置成了 https://corp.example.com,https://proxy.golang.org
。go
命令可能会做如下的请求:
- 请求到
https://corp.example.com/
(并行进行):- 请求最新版本的
golang.org/x/net/html
- 请求最新版本的
golang.org/x/net
- 请求最新版本的
golang.org/x
- 请求最新版本的
golang.org
- 请求最新版本的
- 如果所有对
https://corp.example.com/
的请求有 404 或 410,请求到https://proxy.golang.org/
:- 请求最新版本的
golang.org/x/net/html
- 请求最新版本的
golang.org/x/net
- 请求最新版本的
golang.org/x
- 请求最新版本的
golang.org
- 请求最新版本的
找到合适的模块后,go
命令将添加一个新的 需求 与新模块的路径和版本到主模块的 go.mod
文件。这确保了将来加载相同的包时,将在相同的版本中使用相同的模块。如果解析后的包不是由主模块中的包导入的,则新需求会有一个新的 // indirect
注释.
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: