go-book/src/project-structure/module/index.md
2023-09-10 10:31:14 +08:00

88 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Go Module 基本概念
Module 是 Go 语言中引入用来管理项目依赖的。与包(`package`的概念不一样Module 实际上是一个完整的 Go 项目,其主要提供其他 Go 语言项目使用其中定义的功能。
一个项目中所使用到的所有 Module 都在`go.mod`文件中声明,这一点在之前的`go mod`命令介绍中已经提到过了。程序的每一个依赖库在`go.mod`文件中都是通过依赖库的模块路径module path来区分的。因为模块的模块路径基本上都是 URL 的一部分,所以基本上是可以保证每一个依赖库都拥有唯一的模块路径的。
为了解决引入 Go Module 之前 Go 项目管理中经常出现的依赖库版本混乱的问题,`go.mod`文件中除了声明依赖库的模块路径以外,还需要声明所依赖的依赖库的版本。可以说 Go Module 的引入基本上解决了 Go 项目中依赖管理的问题。
## 模块路径
模块路径是一个模块的规范性名称,也是模块内所有包的前缀。模块路径除了可以定义一个模块以外,一般说来还可以用来寻找模块的出处。通常一个模块的模块路径包括了模块所在的存储库根路径,所在存储库中的路径,以及一个大版本号后缀。
```admonish tip
模块路径中的大版本号后缀通常在Version 2及以上才会使用并且要使用固定书写方法。
```
模块路径的组成通常有以下几个规范。
1. 存储库根路径是模块路径中与开发模块的版本控制存储库的根目录相对应的部分。大多数模块都是在其存储库的根目录中定义的,因此这通常是整个路径。例如`golang.org/x/net`就是一个存储库的根路径,同时也是模块的完整路径。
1. 如果模块不在存储库的根路径中定义,那么模块在存储库根路径中的子目录名称就将成为模块路径的一部分。例如`golang.org/x/tools/gopls`就是存储库`golang.org/x/tools`中的一个名为`gopls`的子目录。
1. 如果模块使用 Version 2 或以上的大版本号发布,那么就需要在模块路径后附加大版本号后缀,大版本号后缀的格式统一为`vN`,例如 Version 2 的大版本号后缀为`v2`。
一个比较常见的复杂示例是 PostgreSQL 数据库的驱动,其模块路径为`github.com/jackc/pgx/v5`,这说明其发布的大版本号是 Version 5如果要引入其下的子目录模块那么就需要使用`github.com/jackc/pgx/v5/pgxpool`的格式,但是实际上`pgxpool`目录是存储库根目录下的子目录。从这个示例可以比较清楚的看出 Go 里模块路径是如何组成的。
## 版本标识
在模块路径中虽然已经附加了大版本后后缀,但是这并不能提供更加具体的依赖库版本定义。模块的版本标识能够确定一个模块的不可变快照,从而使依赖库稳定下来。
Go 要求模块的版本标识遵守[Semantic Versioning 2.0.0(语义版本)](https://semver.org/spec/v2.0.0.html)的规范,并且以小写的字母`v`开头。语义版本规范简而言之就是以下这一套非常简单的规范:语义版本由`.`分隔的三个非负整数组成,从左到右依次是主要版本、次要版本和修补版本,例如`v0.0.0`、`v1.2.3`等。在修补版本之后还可以增加一个由`-`分隔的预发布字符串标记,或者是一个由`+`分隔的元数据字符串标记,例如`v1.2.30-Pre`、`v4.7.0+meta`。语义版本中的每一个部分都标识了当前版本与之前版本之间的兼容关系,它们之间一般会有以下规律。
1. 当 **主要版本** 增加的时候,**次要版本** 和 **修补版本** 都必须置为`0`,此时表示当前版本中已经发生了不兼容之前版本的更改。
1. 如果程序中发生的修改能够兼容之前的版本,例如增加了新功能,但原有功能保持不变,那么 **次要版本** 就必须增加,同时 **修补版本** 要置为`0`。
1. 如果程序中对外的公共接口没有发生更改,只是修复了一些错误或者做了一些优化的工作,那么就只需要增加 **修补版本**。
1. 预发布版本表示其在发布版本之前,例如`v1.2.0-pre`实际上位于`v1.2.0`之前,因为它是一个预发布版本。
1. 元数据标记通常是为了比较版本之间的不同,元数据标记在`go.mod`中会被记录,但是在处理过程中会被忽略。
```admonish tip
我们经常会看到主要版本为`0`的版本号,这一般表示当前版本并不稳定,例如`v0.2.0`可能与`v0.2.0`不兼容。而带有预发布标记的版本可能与发布版本之间不兼容,例如`v1.3.0-alpha`可能与`v1.3.0-beta`和`v1.3.0`都不兼容。
```
### 伪版本号
伪版本号是 Go 为了更好的管理 Go 项目的依赖提出的,它的诞生实际上是一种对于历史和现实的妥协。在 Go Module 推出之前,绝大多数的依赖库都没有采用基于语义版本规范的版本发布策略,所以在 Go Module 中也就无法通过版本标识来确定项目所需的依赖库版本。在另一方面,如果我们在调试一个尚未发布的库时,这个库也不会有一个正式的语义化版本号存在,在这种情况下,也是需要一种手段来实现最新版本的依赖库内容定位的。
但是 Go 语言最开始的设计提供了一个便于解决这个问题的优势Go 语言项目的依赖库基本上都是位于各个版本管理存储库(版本库)中的。这就意味着项目所需要的任何一个版本,在存储库中都是可以被定位和找到的。那么要解决这个问题的关键,就是如何定义一套规则来定位这些版本。
每一个伪版本号都具有固定的三个组成部分。
1. 一个基本的版本号,这个版本号的形式通常是`vX.0.0`或者`vX.Y.Z-0`。这个版本号来自于存储库当前提交Commit之前一个可用的语义化版本标签tag。如果存储库中没有一个可以使用的语义化版本标签那么这个版本号通常就会变成`v0.0.0`。
1. 一个格式为`yyyyMMddhhmmss`的时间戳,这个时间戳是创建提交的时间。
1. 一个提交标识符,格式一般是一个 12 个字符长的字符串,如果使用的存储库是 Git那么这个标识符就是提交的 Hash 标记的前 12 个字符;如果使用的存储库是 Subversion那么就会是一个由`0`填充的字符串。
每一个伪版本号根据其基础版本的不同,都会是以下三种形式之一,这些伪版本号的形式确保了它一定高于其基础版本号,而低于下一个发布版本号。
1. 格式`vX.0.0-yyyyMMddhhmmss-abcdefabcdef`会使用在没有任何已知版本的情况下。此时描述基础版本的`X`必须与模块的主版本后缀匹配,也就是模块路径中出现的`/v2`、`/v5`等主版本标记。如果库没有发布过,那么这个基础版本号`X`就可以是`0`。
1. 当基础版本为`vX.Y.Z-pre`的预发布版本时,伪版本号就会变成`vX.Y.Z-pre.0.yyyyMMddhhmmss-abcdefabcdef`的格式。
1. 当基础版本是`vX.Y.Z`的发布版本时,那么高于基础版本的伪版本号就会变成`vX.Y.(Z+1)-0.yyyyMMddhhmmss-abcdefabcdef`的格式。
```admonish caution
多个伪版本号可能会通过不同的基础版本引用存储库中的同一个提交,这在一个库的开发过程中是很正常的情况。
```
从伪版本号的格式可以看出来这种伪版本号不是手动可以完成输入的Go 中的不少命令都可以对给定的依赖库进行访问并生成其对应的伪版本号。例如`go list -m -json example.com/mod@abcd1234`。
### 主版本号后缀
当开始一个新的主版本号的变化时,模块路径也需要随之发生一些变化,其中最明显的一个变化就是模块路径上需要增加主版本号后缀。这个主版本号后缀非常容易理解,例如依赖库`example.com/mod`是库的第一版的模块路径的话,那么第二版的模块路径会变成`example.com/mod/v2`。
```admonish tip 模块引入兼容规则
如果新库的模块路径与旧库的模块路径相同,那么新库旧必须与旧库完全兼容。
```
根据模块引入兼容规则在模块路径中增加一个主版本号后缀就可以使新版本库的模块路径与旧版本库的模块路径不同这样也就使新版本的库不必与旧版本的库兼容了。但是在引入的时候Go 会自动处理这个后缀,库依旧还是使用模块路径中主版本后缀之前的那一部分作为库的引入名称。
```admonish caution
主版本号后缀只能从`v2`开始标记,当库的版本是`v0`或者`v1`时,是不允许在模块路径上增加主版本后缀的。
```
主版本号后缀允许模块的多个主版本同事共存于同一个项目构建中,换句话说就是在一个项目里允许同时使用同一个依赖库的使用不同主版本号后缀标记的不同版本。这种情况在升级项目依赖时维持项目原有功能的稳定是十分必要的。
## 包是如何被解析的?
一个依赖库在项目中被使用时,引入的包并不一定就是`go.mod`中列举的模块路径,还有可能是依赖库中的子级包路径。这时就有必要了解一下 Go 是如何解析和寻找包的。
Go 首先会检查`go.mod`依赖列表中是否存在匹配表路径前缀的模块。例如在代码中引入了包`exmaple.com/a/b`,那么 Go 就会检查模块`example.com/a`是否存在于依赖列表中,如果依赖列表中存在`example.com/a`,接下来 Go 将检查其中是否包含目录`b`且其中至少存在一个扩展名为`.go`的文件。
如果没有一个模块能够提供一个符合条件的包,或者有两个或以上的模块提供了这个包,那么 Go 命令将报出错误。