Compare commits

..

No commits in common. "c3da564596cf423e34c71001540317f87c2a8360" and "ee356fd4aa37fc582ef45a85a8cf2e572b7d37da" have entirely different histories.

5 changed files with 4 additions and 264 deletions

View File

@ -18,9 +18,9 @@
- [创建一个项目](./project-structure/create.md) - [创建一个项目](./project-structure/create.md)
- [用包来组织代码](./project-structure/package.md) - [用包来组织代码](./project-structure/package.md)
- [可见性](./project-structure/visiblity.md) - [可见性](./project-structure/visiblity.md)
- [Go Module](./project-structure/module/index.md) - [Go Module]()
- [go.mod 的组织](./project-structure/module/mod-file.md) - [go.mod 的组织]()
- [依赖版本的发布](./project-structure/module/release.md) - [依赖版本的发布]()
- [Go 基本语法]() - [Go 基本语法]()
- [基本数据类型]() - [基本数据类型]()
- [表达式]() - [表达式]()

View File

@ -1,6 +1,6 @@
# `go mod`命令 # `go mod`命令
自从 Go 1.13 正式发布 Go Module 以后,基本上绝大多数的 Go 程序都采用了模块化的组织方式。Go Module 的核心概念就是一个 Go 程序,不管是可执行的二进制程序还是一个可以被其他程序所使用的依赖包,都是以一个模块的形式存在的。 自从 Go 1.13 正式发布 Go Module 以后,基本上绝大多数的 Go 程序都采用了模块化的组织方式。Go Module 的核心概念就是忙完一个 Go 程序,不管是可执行的二进制程序还是一个可以被其他程序所使用的依赖包,都是以一个模块的形式存在的。
因为 Go 1.0 在发布的时候已经基本上固定了依赖包的获取和缓存等机制,所以 Go Module 也只能在这个机制上进行改进。接下来会有专门的一章来具体的对 Go Module 中一些常见的概念和应用进行说明。这里先简要的介绍一下`go mod`命令提供的功能。 因为 Go 1.0 在发布的时候已经基本上固定了依赖包的获取和缓存等机制,所以 Go Module 也只能在这个机制上进行改进。接下来会有专门的一章来具体的对 Go Module 中一些常见的概念和应用进行说明。这里先简要的介绍一下`go mod`命令提供的功能。

View File

@ -1,87 +0,0 @@
# 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 命令将报出错误。

View File

@ -1,164 +0,0 @@
# 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 SelectionMVS策略来解析这些依赖并最终生成构建列表。从理论上来说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`之间的版本都不会被获取到。

View File

@ -1,9 +0,0 @@
# 依赖版本的发布
发布一个模块的指定版本并不是一个困难的事情,只需要了解了 Go 命令是如何去解析模块的版本的,就可以非常轻松自如的发布自己项目模块的不同版本。
Go 在设计之初就把 Github 作为了其所有依赖的存储库,所以对于模块路径和版本的解析也都是几乎完全针对 Git 设计的。对于模块版本的解析,一个最简单也是使用最普遍的情况就是如果一个模块被放置在存储库的根目录中,那么就可以通过 Git 版本库的 tag 功能来发布模块的版本。此时 tag 的名称只需要是前面所叙述的语义化版本即可。
如果模块是在存储库中的子目录中定义的,那么存储库中的 tag 名称就必须增加子目录名称作为版本号前缀。例如 Go 工具库`golang.org/x/tools/gopls`就是在其存储库`golang.org/x/tools`的子目录`gopls`中定义的,那么`gopls`在发布的时候,其 tag 名称就必须为`gopls/v0.4.0`的形式。
如果项目没有发布在 Github 上,那么只要项目使用的是基于 Git 的存储库,那么都可以使用这个方法来发布项目的版本。