From 2ae3e460677dee3904cf951d50a46d6f9e3bce51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Sun, 10 Sep 2023 17:08:14 +0800 Subject: [PATCH] =?UTF-8?q?post:=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90go.mod?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=9A=84=E7=BB=84=E6=88=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SUMMARY.md | 2 +- src/project-structure/module/mod-file.md | 164 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/project-structure/module/mod-file.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 655c106..3922b34 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -19,7 +19,7 @@ - [用包来组织代码](./project-structure/package.md) - [可见性](./project-structure/visiblity.md) - [Go Module](./project-structure/module/index.md) - - [go.mod 的组织]() + - [go.mod 的组织](./project-structure/module/mod-file.md) - [依赖版本的发布]() - [Go 基本语法]() - [基本数据类型]() diff --git a/src/project-structure/module/mod-file.md b/src/project-structure/module/mod-file.md new file mode 100644 index 0000000..42730da --- /dev/null +++ b/src/project-structure/module/mod-file.md @@ -0,0 +1,164 @@ +# go.mod 文件的组织 + +`go.mod`文件是一个 UTF-8 编码的纯文本文件,这个文件会随着`go mod init`命令的执行自动创建。`go.mod`文件中,每一行定义一个指令,每个指令由一个关键字和一个参数组成。 + +以下是一个`go.mod`文件中常见的形式。 + +``` +module example.com/my/project + +go 1.20 + +require example.com/other/lib v1.0.2 +require example.com/new/lib/v2 v2.3.4 +exclude example.com/old/lib v1.2.3 +replace example.com/alias/lib v1.4.5 => example.com/origin/lib v1.4.5 +retract [v1.9.0, v1.9.5] +``` + +虽然`go.mod`文件被设计为了便于人类阅读的形式,但是一般来说并不建议趾甲对其进行编辑修改。对`go.mod`文件的编辑修改一般还是建议通过 Go 提供的命令来完成,例如`go get`命令可以增加或者升级、降级指定的依赖,`go mod edit`命令可以提供一些较为低级的编辑功能。 + +## 基本语法定义 + +`go.mod`文件的语法是遵循以下定义的。 + +``` +GoMod = { Directive } . +Directive = ModuleDirective | + GoDirective | + RequireDirective | + ExcludeDirective | + ReplaceDirective | + RetractDirective . +``` + +这里面的,每一个 Directive 都对应一个关键字,它们的具体用法会在后面逐一说明。 + +## 可用的关键字 + +`go.mod`文件中可以使用的关键字主要有以下这些: + +- `module`,模块定义指令。 +- `go`,发行版适配指令。 +- `require`,模块依赖声明指令。 +- `replace`,模块替换指令。 +- `exclude`,排除模块依赖指令。 +- `retract`,模块版本撤回指令。 + +### 模块定义指令 + +模块是使用`module`关键字定义的,其语法定义为: + +``` +ModuleDirective - "module" ( ModulePath | "(" newline ModulePath newline ")" ) newline . +``` + +在每个`go.mod`文件中,仅可以存在一个`module`指令。根据上面的语法规则定义,`module`指令后面会接一个模块路径,这个模块路径就是当前项目的模块路径。虽然语法定义中声明了可以使用`()`,但是其中也只能声明一个模块路径。 + +`module`关键字所定义的内容其实并不需要手动输入,在使用`go mod init`命令创建项目的时候,模块路径就已经被自动创建好了。如果我们计划废弃目前的这一个模块,那么可以使用`// Deprecated: `注释对其进行标记,就像是下面这样。 + +``` +// Deprecated: 废弃提示信息。 +module example.com/mod +``` + +### 适应发行版定义 + +由于 Go 的不同发行版之间总会有一些功能上的差别,当项目使用了了这具有差别的内容时,就对这一个发行版产生了强依赖关系,如果使用其他版本的发行版来编译项目,就有可能出现编译失败的情况。`go`指令就是用来定义当前的模块可以适配的最低发行版版本的。`go`只要的语法定义为: + +``` +GoDirective = "go" GoVersion newline . +GoVersion = string | ident . +``` + +在 Go 1.21 之前的版本中,`go`指令只是一个指示性的指令,但是在 Go 1.21 版本之后,`go`指令就变成一个强制性的指令了,在定义适配的发行版以后,Go 工具链就会拒绝使用适配更新的发行版的模块了,也就是 Go 工具链会确保项目所使用的所有模块都能匹配当前指定的发行版。 + +### 模块依赖声明 + +模块依赖声明是整个`go.mod`文件中内容最多,也是比较核心的内容。`go.mod`文件中对于项目依赖的模块的声明是他国`require`指令来完成的,`require`指令的语法格式如下: + +``` +RequireDirective = "require" ( RequireSpec | "(" newline { RequireSpec } ")" newline ) . +RequireSpec = ModulePath Version newline . +``` + +这套语法体现在`go.mod`文件中,就是生面示例中的样子。 + +``` +require example.com/other/lib v1.0.2 +require example.com/new/lib/v2 v2.3.4 +``` + +在 Go 中,更常见的样式是将使用同一个关键字的内容使用`()`包裹起来,也就是下面这种样子。 + +``` +require ( + example.com/other/lib v1.0.2 + example.com/new/lib/v2 v2.3.4 +) +``` + +Go 在处理所需的每个模块版本的时候,会使用最小版本选择(Minimal Version Selection,MVS)策略来解析这些依赖,并最终生成构建列表。从理论上来说,MVS 在项目的有向图上对项目的依赖和依赖的版本进行分析,有向图上的每一个顶点都代表一个模块的版本,每一条边都代表一个使用`require`指令指定的依赖项所需的最低版本。MVS 从图上的主要模块,也就是项目模块开始,对整个图进行遍历,在遍历过程中,Go 会跟踪每个模块所需的最高版本。在遍历结束以后,由这些模块所要求的最高版本所组成的构建列表就是满足项目构建的最低版本要求。 + +在一些项目中的`go.mod`里可能还会在`require`指令中看到在版本号后面标记的`// indirect`注释,这个注释是 Go 命令在分析项目依赖的时候自动添加的,`// indirect`表示这行依赖定义的是一个间接依赖,也就是说这个依赖项不是在项目的主模块中被直接引用的。在`go.mod`中出现`// indirect`标记的依赖项主要是由于以下两个原因。 + +1. 如果`go`指令指定使用 1.16 或以下的发行版,那么当一个依赖所使用的版本高于项目其他依赖中声明的版本时,Go 命令就会增加一个间接依赖。这种情况主要发生在使用`go get -u`进行显式升级或者是使用`go mod tidy`进行依赖项整理,或者是导入了一个不存在`ge.mod`文件的依赖项的时候发生。 +1. 在指定使用 Go 1.17 级以上的发行版时,Go 命令为每一个模块都添加了一个间接依赖。这样做的目的主要是未来支持项目依赖图的修剪和延迟模块加载。 + +### 排除模块声明 + +顾名思义,排除模块指令的功能就是用来防止指定模块被 Go 命令加载的。排除指令的语法定义如下: + +``` +ExcludeDirective = "exclude" ( Exclude | "(" newline { ExcludeSpec } ")" newline ) . +ExcludeSpec = ModulePath Version newline . +``` + +`exclude`指令只能使用在当前项目的`go.mod`中,并且在当前项目作为依赖库被其他项目所引用的时候是不会被使用的。 + +### 替换模块声明 + +模块替换指令是`go.mod`中一个比较常用的指令。它的主要作用是把特定版本或所有版本的内容替换为其他位置的内容。这个替换可以使用另一个模块路径和版本或者是本地文件路径。模块替换指令主要用在以下几种情况里: + +1. 模块定义路径与其实际存储库路径不统一。这种情况下 Go 命令无法通过给定的模块定义路径定位模块的存储库及其所在,所以就需要通过替换指令将其更改为模块实际的存储库路径。在一个模块放置在私有存储库中时,这种情况可能会比较常见。 +1. 使用了一个模块的 Fork 版本。一个模块的 Fork 版本通常会在其原有功能上增加一些修改,但其实际存储库路径不可能与原版模块的路径相同,所以在使用的时候就需要替换一下。 +1. 项目的主模块分拆成了多个模块在本地开发,各个子模块虽然拥有自己的模块路径,但是本地的代码会频繁更新,使用存储库路径会使开发流程变得复杂。 + +替换指令的语法定义如下: + +``` +ReplaceDirective = "replace" ( ReplaceSpec | "(" newline { ReplaceSpec } ")" newline ) . +ReplaceSpec = ModulePath [Version] "=>" FilePath newline | + ModulePath [Verion] "=>" ModulePath Version newline . +``` + +模块的替换根据箭头`=>`左右的内容不同,会存在以下不同的模块替换策略。 + +1. 箭头左侧指定了某一个版本,则仅替换模块的指定版本,否则则会替换所有版本的模块。 +1. 如果箭头右侧是一个绝对路径或者相对路径,且这个路径下存在`go.mod`文件,那么它就会被用来替换指定模块路径,否则将会被忽略。 +1. 如果箭头右侧不是本地路径,那么就必须是一个有效的模块路径。这种情况下必须指定一个具体的模块版本。 + +```admonish caution +无论是使用本地路径还是模块路径进行模块的替换,如果替换模块中具备`go.mod`文件,那么其中的模块定义指令中的模块路径必须与其替换的模块路径匹配。也就是说,用于替换的模块,在其中具备`go.mod`文件的时候,只能替换模块路径为`go.mod`文件中`module`指令定义的模块路径的模块,不可随意替换。 +``` + +### 版本撤回指令 + +`retract`指令通常用来标识当前项目所提供的不应该被其他模块所使用的版本。如果当前项目的发布版本出现问题,不应该备注其他项目中使用`go get`或者`go get -u`获取到,那么就需要在当前项目的`go.mod`文件中使用`retract`指令标注这些不可靠的版本,以防止这下版本被获取。 + +版本撤回指令的语法格式如下: + +``` +RetratDirective = "rretract" ( RetractSpec | "(" newline { RetractSpec } ")" newline ) . +RetractSpec = ( Version | "[" Version "," Version "]" ) newline . +``` + +例如可以像以下这样来防止正在开发中的`v0`版本模块被依赖。 + +``` +retract ( + [v0.0.0, v0.9.9] +) +``` + +这个示例为从`v0.0.0`到`v0.9.9`之间的版本都不会被获取到。