Compare commits

...

7 Commits

Author SHA1 Message Date
徐涛
44caae4fc8 post:暂时完成基本类型。 2023-09-12 16:04:05 +08:00
徐涛
9e78e85613 fix:修复admonish提示中的错误。 2023-09-11 17:22:36 +08:00
徐涛
deef0f833f post:基本完成标识符和关键字。 2023-09-10 21:45:19 +08:00
徐涛
c3da564596 post:基本完成版本发布。 2023-09-10 17:47:14 +08:00
徐涛
2ae3e46067 post:基本完成go.mod文件的组成。 2023-09-10 17:08:14 +08:00
徐涛
26d4780842 post:完成模块基本概念。 2023-09-10 10:31:14 +08:00
徐涛
ed20d1b187 fix:纠正一些错字。 2023-09-08 15:57:41 +08:00
10 changed files with 696 additions and 7 deletions

View File

@@ -18,12 +18,14 @@
- [创建一个项目](./project-structure/create.md)
- [用包来组织代码](./project-structure/package.md)
- [可见性](./project-structure/visiblity.md)
- [Go Module]()
- [go.mod 的组织]()
- [依赖版本的发布]()
- [Go 基本语法]()
- [基本数据类型]()
- [表达式]()
- [Go Module](./project-structure/module/index.md)
- [go.mod 的组织](./project-structure/module/mod-file.md)
- [依赖版本的发布](./project-structure/module/release.md)
- [Go 基本语法](./grammar/index.md)
- [标识符与关键字](./grammar/identifier-keyword.md)
- [常量与变量](./grammar/constant-variable.md)
- [基本数据类型](./grammar/basic-types.md)
- [表达式](./grammar/expression.md)
- [语句]()
- [函数]()
- [指针与引用]()
@@ -36,6 +38,7 @@
- [结构体]()
- [接口]()
- [泛型]()
- [类型定义]()
- [错误处理]()
- [反射]()
- [并发]()

View File

@@ -1,6 +1,6 @@
# `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`命令提供的功能。

109
src/grammar/basic-types.md Normal file
View File

@@ -0,0 +1,109 @@
# 基本数据类型
Go 中的数据类型主要有布尔、数字、字符串、数组、切片、结构体、指针、函数、接口、Map 和通道。这一章中将主要介绍各个基本类型的语法定义和基本功能,其具体的使用方法将在后文中的独立章节中进行具体说明。
在 Go 中数据类型的语法定义如下:
```
Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeArgs = "[" TypeList [ "," ] "]" .
TypeList = Type { "," Type } .
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType | SliceType | MapType | ChannelType .
```
在这个语法定义中,`TypeArgs`表示一个类型参数,是后面索要介绍的泛型中使用的,目前可以暂时不必理会。目前需要从语法定义中了解的只有 Go 中常用的数据类型的书写方法。
## 布尔类型
布尔类型是 Go 语言中最简单也是使用最广的类型,其类型标识符为`bool`,可取值为预定义的常量`true``false`,分别对应真值和假值。
## 数字类型
数字类型是整型、浮点型、复数型三种类型的统称,它们分别用于表示整数、浮点值和复数值。用于表示数字类型的标识符都是 Go 语言预先定义好的,全局有效的。数字类型主要有以下这些。
| 类型标识符 | 指代类型 | 长度(位) | 最小值 | 最大值 |
| ------------ | ---------------------------- | ---------- | -------------------- | -------------------- |
| `uint8` | 无符号 8 位整型 | 8 | 0 | 255 |
| `uint16` | 无符号 16 位整型 | 16 | 0 | 65535 |
| `uint32` | 无符号 32 位整型 | 32 | 0 | 4294967295 |
| `uint64` | 无符号 64 位整型 | 64 | 0 | 18446744073709551615 |
| `int8` | 有符号 8 位整型 | 8 | -128 | 127 |
| `int16` | 有符号 16 位整型 | 16 | -32768 | 32767 |
| `int32` | 有符号 32 位整型 | 32 | -2147483648 | 2147483647 |
| `int64` | 有符号 64 位整型 | 64 | -9223372036854775808 | 9223372036854775807 |
| `float32` | IEEE--754 32 位浮点 | 32 | | |
| `float64` | IEEE-754 64 位浮点 | 64 | | |
| `complex64` | `float32`型实数和虚数复数 | 64 | | |
| `complex128` | `float64`型实数和虚数复数 | 128 | | |
| `byte` | 单字节整型,`uint8`别名 | 8 | | |
| `rune` | 字符型,`int32`别名 | 32 | | |
| `uint` | 机器架构长度无符号整型 | 32 或 64 | | |
| `int` | 机器架构长度有符号整型 | 32 或 64 | | |
| `uintptr` | 足以存放内存地址的无符号整型 | | | |
## 字符串类型
字符串类型是一个可能为空的字节序列,是一个字符值的集合。字符串类型使用`string`预定义标识符声明。字符串的内容一旦创建即不可更改,
字符串中的元素可以通过下标访问,其中元素的索引从 0 开始。
## 数组类型
数组类型是由一个单一类型元组组成的序列类型,数组中元素的数量称为数组的长度。数组类型的定义语法格式如下。
```
ArrayType = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .
```
根据数组类型的语法定义,一个数组类型可以使用`[7]int``[20]byte`来表示。这里需要注意的是,数组类型中的长度是数组类型的一部分,必须是能够确定的`int`类型非负常数,数组一旦定义,其长度在数组的作用域内就不再可变。数组总是一维的,但是可以通过组合来形成多维数组,例如`[4][5]int`
数组中的元素可以通过整型索引来访问。
## 切片类型
之前介绍过的数组类型是一个长度固定不可变的序列类型,切片则是一个不固定长度的序列类型。与数组一样,切片也是通过下标去访问其中元素的。切片类型的定义语法如下。
```
SliceType = "[" "]" ElementType .
```
根据切片类型的语法定义,一个切片类型可以使用`[]int``[]byte`来表示。与数组类型不同的是,切片类型的类型中没有切片长度的部分。但是与数组一样,切片也是一维的,并且可以通过组合形成多维切片,但是由于切片的长度是可以动态变化的,所以多维切片中的每一个内部切片都需要单独完成初始化。
## 结构体
结构体是一个由多种类型组合在一起形成的一个能够记录和描述复杂内容的类型。其中每一个命名元素都有自己端粒的类型,被称为字段。结构体的具体语法定义将放在后面专门讲述结构体的章节详细说明。
## 指针类型
指针无论在哪个语言中都是一个非常神奇和灵活的类型,它在理论上可以指向任意的内存地址。在 Go 中一个指针只能指向给定类型的变量地址,这个给定的类型被称为指针的基类型。指针类型的语法定义如下。
```
PointerType = "*" BaseType .
BaseType = Type .
```
根据指针类型的语法定义,一个指针类型可以使用`*int``*[]byte`的形式来表示。
## Map 类型
Map 类型主要功能是用来保存键值对是一种无序的序列由两种类型组成其中作为值的类型也被称为元素类型另一种则作为值的索引也被称为键类型。Map 类型的语法定义如下。
```
MapType = "map" "[" KeyType "]" ElmentType .
KeyType = Type .
```
根据 Map 类型的语法定义,以下 Map 类型的书写都是合法的:`map[string]int``map[string]iterface{}`
## 通道类型
Go 里提供的通道类型提供了一种机制,可以在不同的协程和线程之间传输指定类型的元素来实现通信。用于实现这种通信功能的类型就是通道。通道的语法定义如下。
```
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
```
具体通道类型的使用将在后续的独立章节中介绍。

View File

@@ -0,0 +1,178 @@
# 常量和变量
常量和变量是程序中所要操作的数据的载体。常量和变量的区别其实主要在于其中所承载的数据是否可变,数据的可变性决定了承载指代它们的容器,并且决定了它们在程序中能够发挥的作用。
## 常量
常量顾名思义就是不可变的量,在程序中定义的常量一般都携带有特殊的含义。根据常量类型的不同,一般可以按参照类型分为布尔常量、字符常量、整数常量、浮点常量、复数常量和字符串常量,其中字符常量、整数常量、浮点常量和复数常量一般统称为数字常量。
常量主要是通过字面量、其他的常量标识符、常量表达式等定义的,其定义原则就是用来定义常量的值在 Go 项目编译的时候是能够确定的,只要是能够符合这个条件的值,都可以用来定义常量。常量可以是类型化的也可以是非类型化的,像是字面量、`true``false``iota`或者是包含非类型化常量的常量表达式都是非类型化的。常量的类型可以在常量定义表达式中显式声明,也可以通过在赋值表达式中为变量赋值时隐式声明。
```admonish tip
隐式类型声明都是通过类型推断完成的。如果给定常量值不能匹配常量或者变量的类型那么Go将产生一个错误。如果目标类型是一个类型参数那么这个常量值将会被转换成匹配类型参数的非常量值。
非类型化的常量并不等于常量没有类型,非类型化只是指常量的类型是隐式声明的而已。
```
数字常量通常都标识任意精度的准确值,不会存在溢出的情况,所以不会存在负零、无穷大和非数字值的常量。尽管数字常量在 Go 中拥有任意精度,但是在编译器内它们的精度还是有限制的。
程序所使用的常量都是通过`const`关键字定义的,其语法定义如下:
```
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .
```
以下是一些常见的常量定义,以及它们所定义出的内容类型。
```go
const a = 2 + 3.0 // a = 5.0,非类型化浮点常量
const b = 15 / 4 // b = 3非类型化整型常量
const c = 15 / 4.0 // c = 3.75,非类型化浮点常量
const θ float64 = 3 / 2 // θ = 1.0,类型化双精度浮点常量,
const λ float64 = 3 / 2. // λ = 1.5,类型化双精度浮点常量,
const d = 1 << 3.0 // d = 8非类型化整型常量
const e = 1.0 << 3 // e = 8非类型化整型常量
const f = int32(1) << 33 // 非法常量将常量值赋予int32类型常量是发生了溢出
const g = float64(2) >> 1 // 非法常量float64(2)是一个类型化的常量
const h = "foo" > "bar" // h = true非类型化布尔常量
const j = true // j = true非类型化布尔常量
const k = 'w' + 1 // k = 'x',非类型化字符常量
const l = "hi" // l = "hi",非类型化字符串常量
const m = string(k) // m = "x",类型化字符串常量
const Σ = 1 - 0.7i // 非类型化复数常量
const Δ = Σ + 2.0e-4 // 非类型化复数常量
const φ = iota * 1i - 1 / 1i // 非类型化复数常量
const ic = complex(0, c) // ic = 3.75i,非类型化复数常量
const iθ = complex(0, θ) // iθ = 1i类型化复数常量
```
在上面这些示例中,阅读时清留意常量的类型化和非类型化的区别。
根据`const`语法定义,在定义常量的时候还可以通过常量列表来进行批量定义,例如:
```go
const a, b, c = 1, 3, "hi" // a = 1b = 3c = "hi",分别是非类型化整型常量和非类型化字符串常量
const u, v float32 = 0, 3 // u = 0.0v = 3.0,都是类型化单精度浮点常量
```
### iota
在使用带括号的常量声明列表时,用于定义常量的表达式可以从第二个常量(`ConstSpec`)处省略,这样形成的空列表就相当于前面第一个非空表达式列表及其类型。所以省略表达式列表就相当于重复之前的表达式列表。这种用法通常都会结合`iota`这个预定义标识符来使用。
`iota`标识连续的非类型化整数常量,它其实是一个索引,值从`0`开始,可以用来构建一组相关的常量。在每一个常量列表开始的时候,`iota`的索引都会被重置为`0`。
以下是几个利用`iota`定义常量列表的例子。
```go
const (
a = 1 << iota // a = 1此时iota = 0
b // b = 2此时iota = 1
c // c = 4此时iota = 2
)
const (
a = 1 << iota // a = 1
b = 1 << iota // b = 2
c = 3 // c = 3此处没有使用iota的值
d = 1 << iota // d = 8此时iota = 3
)
const a = iota // a = 0
const b = iota // b = 0每一个const定义序列开始iota的索引值就被重置了
```
如果在常量定义列表中,有一些值是不需要的,可以使用`_`来跳过,例如:
```go
const (
a = iota // a = 0
b // b = 1
_ // 空白标识符可以跳过某一个值的获取此处就跳过了iota = 2
d // d = 3
)
```
```admonish tip
注意Go中的开放性规则以上示例中的所有内容都是仅能在当前包中访问的也就是私有的。如果需要在其他包中可以访问常量的开头字母必须是大写例如`const A = 5`。
`const`关键字引领的常量定义是Go语言中的一等公民可以直接在包中声明。
```
## 变量
变量的主要用途是保存值的存储位置。是的,变量保存的不是值本身,而是值在内存中的位置,这里请不要是指针的概念混淆了。一个变量所能够指代的值是由其类型决定的。
Go 会在编译时对**变量声明**、**函数参数**、**函数返回结果**、**函数声明**、**函数签名**预留命名变量存储空间,而调用内置函数`new`或者获取一个复杂字面量的地址,都会在运行时为变量分配一个存储空间。
变量都是通过`var`关键字声明的,其语法定义如下:
```
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
```
根据`var`关键字的语法定义,以下示例中的变量定义都是合法的。
```go
var i int // 变量i仅声明自动使用零值初始化
var U, V float64 // 可悲外部访问的变量U、V仅声明自动使用零值初始化
var k = 0 // 使用赋值表达式初始化变量kk的类型由提供的值推断
var x, y float64 = -1, -2 // 同时定义双精度浮点类型变量x和y并对其赋值
var (
a int
r, s, t = 2.0, 3, "world" // 利用赋值语句同时定义不同类型的变量
)
```
在上面的示例中,有一些变量在声明的时候没有初始化,此时这个变量是使用其零值进行默认初始化的。所以在声明变量的时候,需要注意每个类型的零值是什么样的。
变量声明中同样也可以使用`_`空白标识符来略过一些赋值过程中不需要的内容。例如在判断一个变量的类型的时:`var _, ok = x.(T)`。
```admonish tip
`var`关键字引领的变量定义是Go语言中的一等公民可以直接在包中声明。
```
### 简短变量定义
Go 除了可以使用`var`关键字声明和定义变量以外,还提供了一种简短的变量声明方式,这种简短的变量声明方式在 Go 语言代码中被广泛的使用。这种简短的变量声明语法定义如下。
```
ShortVarDecl = IdentifierList ":=" ExpressionList .
```
使用这种简短的变量定义方式,上面的变量定义示例就可以改写成以下样子。
```go
x, y := -1, -2
k := 0
r, s, t := 2.0, 3, "world"
_, ok := x.(T)
```
使用这种简短变量定义时需要注意的是,简短变量定义中不支持书写变量的类型,也就是说变量的类型是完全依靠推断的。而且与使用`var`关键字的变量声明不同,简短变量声明只能够使用在函数内部。
## 作用域
常量和变量的声明都是将一个非空白标识符绑定到常量和变量的过程,实际上这个声明的过程除了可以绑定常量和变量以外,还可以绑定类型、类型参数、函数、标签、包等,这些其他内容的声明会在介绍相应内容的时候进行说明。
```admonish caution
Go中所有声明的限制是同一个非空白标识符在同一个块中不能声明两次在同一个包的不同文件中也一样。空白标识符因为不会引入绑定所以它不是一个声明所以其使用次数不受限制。
```
一个标识符的作用范围是声明它的代码区块范围Go 语言使用`{}`来定义一个块。那么对于一个标识符来说,其作用域的确定主要依赖于以下几种规则。
1. Go 语言预定义标识符的作用域恒为程序全局。
1. 在函数之外定义的标识符,作用域为包(`package`)。
1. 被导入包的包名作用域是包含导入声明的文件。
1. 方法的接收器、函数参数、函数返回值变量的作用域是函数体。
1. 函数的类型参数的作用域是从方法接收器开始到函数体末尾。
1. 类型参数标识符的作用域是从类型名称开始直到类型定义结束。
1. 函数内部声明的常量和变量的作用域是从其声明结束开始到包含其声明的块的结束。
1. 函数内部声明的类型标识符的作用域是从类型声明开始直到包含其声明的块的结束。
```admonish tip "作用域遮罩"
如果在一个块的内部构建了一个新的块,而且在这个内部块内声明了与外部块中相同的标识符,那么内部块中声明的标识符就将在其作用域范围内屏蔽外部块的同名标识符,这种情况通常被称为作用域遮罩,也是在声明标识符的时候,比较容易被忽略的一个问题。
```

View File

@@ -0,0 +1 @@
# 表达式

View File

@@ -0,0 +1,97 @@
# 标识符和关键字
标识符和关键字在任何语言的程序中都是组成程序代码的基础元素,它们在程序代码中都有独特且专一的功能。
## 标识符
标识符在代码中主要用来命名程序实体,例如变量、类型等。在 Go 语言中,标识符是一个由一个或者多个字母或数字组成的序列,其第一个字符必须是字母,不能是数字。
标识符的语法定义为:
```
identifier = letter { letter | unicode_digit } .
```
在 Go 语言中,由一些标识符是在全局隐式定义的,它们已经具有了特殊含义的指代,不可被挪用于指代其他内容。以下表格中即是 Go 在全局定义的标识符。
| 标识符 | 类型 | 含义 |
| ------------ | ---- | -------------------------------------------------------- |
| `any` | 类型 | 指代任意类型 |
| `bool` | 类型 | 指代布尔类型 |
| `byte` | 类型 | 指代字节类型 |
| `comparable` | 类型 | 指代可比较类型 |
| `complex64` | 类型 | 指代 64 位复数类型 |
| `complex128` | 类型 | 指代 128 位复数类型 |
| `error` | 类型 | 指代描述错误的接口 |
| `float32` | 类型 | 指代 32 位单精度浮点类型 |
| `float64` | 类型 | 指代 64 位双精度浮点类型 |
| `int` | 类型 | 指代当前机器架构支持长度的整型 |
| `int8` | 类型 | 指代 8 位长度的单字节整型 |
| `int16` | 类型 | 指代 16 位长度的双字节整型 |
| `int32` | 类型 | 指代 32 位长度的四字节整型 |
| `int64` | 类型 | 指代 64 位长度的八字节整型 |
| `rune` | 类型 | 指代一个 UTF-8 码点字符 |
| `string` | 类型 | 指代字符串类型 |
| `uint` | 类型 | 指代当前机器架构支持长度的无符号整型 |
| `uint8` | 类型 | 指代 8 位长度的单字节无符号整型 |
| `uint16` | 类型 | 指代 16 位长度的双字节无符号整型 |
| `uint32` | 类型 | 指代 32 位长度的四字节无符号整型 |
| `uint64` | 类型 | 指代 64 位长度的八字节无符号整型 |
| `uintptr` | 类型 | 指代一个用于保存指针地址的整型 |
| `true` | 常量 | 指代真值 |
| `false` | 常量 | 指代假值 |
| `iota` | 常量 | 指代常量序列计数起始的`0` |
| `nil` | 零值 | 指代空指针 |
| `append` | 函数 | 用于向一个切片中增加元素 |
| `cap` | 函数 | 返回一个切片的容量capacity |
| `clear` | 函数 | 清空 map重置切片中的全部元素Go 1.21 以后可用) |
| `close` | 函数 | 用于关闭通道 |
| `complex` | 函数 | 用于构建一个复数类型 |
| `copy` | 函数 | 用于复制一个切片 |
| `delete` | 函数 | 用于删除 map 中的键值对和切片中的元素 |
| `imag` | 函数 | 获得一个复数的虚数部分 |
| `len` | 函数 | 计算字符串或者切片、数组等可被计数内容的长度(元素数量) |
| `make` | 函数 | 创建一个切片、Map、结构体的实例 |
| `max` | 函数 | 获取给定值中的最大值Go 1.21 以后可用) |
| `min` | 函数 | 获取给定值中的最小值Go 1.21 以后可用) |
| `new` | 函数 | 创建一个指定类型的实例并返回其引用 |
| `panic` | 函数 | 中断程序运行并抛出异常 |
| `println` | 函数 | 在控制台中输出指定内容 |
| `real` | 函数 | 获得一个复数的实数部分 |
| `recover` | 函数 | 捕获`panic`抛出的异常,并使程序从异常状态中恢复 |
### 空白标识符
空白标识符在之前引入包的章节中就已经见识过了,空白标识符就是一个`_`,它在表达式中充当匿名占位符,在声明表达式、赋值表达式中都具有特殊的含义。
## 关键字
关键字实际上也是标识符但是是语言中保留的内容因为其在语言中具有专门的用途和意义所以不能被用作普通的标识符。Go 中的关键字主要有以下这些。
| 关键字 | 功能 |
| ------------- | ---------------------------------------- |
| `break` | 用于退出循环 |
| `case` | 用于定义`switch`语句的分支条件 |
| `chan` | 用于标识通道类型 |
| `const` | 用于定义常量 |
| `continue` | 用于提前结束当前的循环,并开始下一次循环 |
| `default` | 用于定义`switch``select`中的默认分支 |
| `defer` | 用于定义延后执行的语句 |
| `else` | 用于定义`if`语句的分支 |
| `fallthrough` | 用于打通`switch`语句中的多个`case`分支 |
| `for` | 用于定义循环 |
| `func` | 用于定义函数 |
| `go` | 用于启动协程 |
| `goto` | 用于执行强制跳转 |
| `if` | 用于定义条件分支 |
| `import` | 用于引入其他的包 |
| `interface` | 用于定义接口 |
| `map` | 用于定义 map 类型 |
| `package` | 用于定义当前代码文件归属的包 |
| `range` | 用于定义范围边界 |
| `return` | 用于从函数中返回值 |
| `select` | 用于监听和响应通道 |
| `struct` | 用于定义结构体 |
| `switch` | 用于定义复杂分支结构 |
| `type` | 用于定义类型 |
| `var` | 用于定义变量 |

41
src/grammar/index.md Normal file
View File

@@ -0,0 +1,41 @@
# Go 基本语法
任何一门编程语言都是由一些基本的词汇元素组成的Go 语言也不例外。构成 Go 语言程序代码的基本元素归类起来主要是标识符、关键字、操作符、标点符号和文字五类。其中所有的空白字符包括空格、Tab 制表符、回车等,都会在程序编译时被忽略,它们只是程序代码中基本元素的分隔符,而不是代码的基本元素之一。
## 分号
在在大多数语言中,分号都是用来作为一个语句结束的标记的,在 Go 语言中也不例外,但是 Go 提供了一些独特的解析规则使得我们在大部分情况下可以省略分号的书写。
1. 当一行的内容被分解为 Token 时,最后一个 Token 是以下内容的时候,分号会被自动插入。
- 一个标识符;
- 一个整型、浮点型、复数类型、字符或者是字符串的字面量;
- `break``continue``fallthrough`或者是`return`中的一个;
- `++``--``)``]`或者是`}`中的一个。
1. 一个在`)`或者`}`之前的独占一行的复杂语句。
基于这两条规则,绝大多数语句结尾的分号都可以被省略掉,读者可以在实际编程练习中仔细体会一下。
## 代码文档字符集
Go 语言的代码文档是采用 UTF-8 编码格式的,所以在代码文档中基本上可以使用任意合法的 UTF-8 字符。但是 Go 语言在其 EBNF 格式的定义中,还是将代码文档中可以使用的字符划分成了以下几类:
- 换行符,在 EBNF 定义中使用`newline`标识。
- UTF-8 字符,是指所有除了换行符以外的 UTF-8 码位,在 EBNF 定义中使用`unicode_char`标识。
- UTF-8 字母,是指 UTF-8 码位中,所有被归类为字母的码位,在 EBNF 定义中使用`unicode_letter`标识。
- UTF-8 数字,是指 UTF-8 码位中,所有被归类为数字或者数字位的码位,在 EBNF 定义中使用`unicode_digit`标识。
通过以上对于代码文档中可以使用的字符的定义,再结合 Go 语言代码中出现的内容,就可以定义出也以下基本字符和数字。
```
letter = unicode_letter | "_" .
decimal_digit = "0" ... "9" .
binary_digit = "0" | "1" .
octal_digit = "0" ... "7" .
hex_digit = "0" ... "9" | "A" ... "F" | "a" ... "f" .
```
这套定义也就是说,在 Go 语言代码中,所有的合法字母除了包括 UTF-8 中被归类为字母的码位以外,还包括下划线`_`。剩下的四个定义分别对应十进制数字、二进制数字、八进制数字和十六进制数字。
```admonish tip
这些基础的EBNF定义虽然简单但是它是构成后文中其他复杂EBNF定义的基础。
```

View File

@@ -0,0 +1,87 @@
# 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

@@ -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 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

@@ -0,0 +1,9 @@
# 依赖版本的发布
发布一个模块的指定版本并不是一个困难的事情,只需要了解了 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 的存储库,那么都可以使用这个方法来发布项目的版本。