Compare commits

..

No commits in common. "1f2303bd2570323abe52e655273f34e90e700bea" and "1cab115d2c1d9e0e2f10dc6a3f8516cf68ddb5ca" have entirely different histories.

8 changed files with 4 additions and 325 deletions

View File

@ -6,7 +6,6 @@
- [Go 语言简介](./preguide/intro.md) - [Go 语言简介](./preguide/intro.md)
- [环境安装与配置](./preguide/installation.md) - [环境安装与配置](./preguide/installation.md)
- [不一样的编程思路](./preguide/codex.md) - [不一样的编程思路](./preguide/codex.md)
- [Extended Backus-Naur Form](./preguide/enbf.md)
- [基本命令](./basic-cmd/index.md) - [基本命令](./basic-cmd/index.md)
- [go mod](./basic-cmd/mod.md) - [go mod](./basic-cmd/mod.md)
- [go get](./basic-cmd/get.md) - [go get](./basic-cmd/get.md)
@ -14,13 +13,10 @@
- [go build](./basic-cmd/build.md) - [go build](./basic-cmd/build.md)
- [go install](./basic-cmd/install.md) - [go install](./basic-cmd/install.md)
- [go generate](./basic-cmd/generate.md) - [go generate](./basic-cmd/generate.md)
- [项目基本结构](./project-structure/index.md) - [项目基本结构]()
- [创建一个项目](./project-structure/create.md) - [创建一个项目]()
- [用包来组织代码](./project-structure/package.md) - [用包来组织代码]()
- [可见性](./project-structure/visiblity.md) - [可见性]()
- [Go Module]()
- [go.mod 的组织]()
- [依赖版本的发布]()
- [Go 基本语法]() - [Go 基本语法]()
- [基本数据类型]() - [基本数据类型]()
- [表达式]() - [表达式]()

View File

@ -1,45 +0,0 @@
# Extended Backus-Naur Form (ENBF)
Backus-Naur Form (BNF)是一种用于描述编程语言语法的元语言。Extended Backus-Naur Form 是它的一个扩展版本。Go 采用 ENBF 来定义它的语言规范,在 Go 语言官网的语言规范中可以大量的看到 ENBF 的定义。本书中也将直接利用 Go 语言规范中的 ENBF 定义,来说明 Go 中各个语言功能的使用,以确保所有说明的准确。
## ENBF 的基本语法
以下是 ENBF 的一个基本语法列表,这个列表也是使用 ENBF 语法定义的。
```
Syntax = { Production } .
Production = production_name "=" [ Expression ] "." .
Expression = Term { "|" Term } .
Term = Factor { Factor } .
Factor = production_name | token [ "..." token ] | Group | Option | Repetition .
Group = "(" Expression ")" .
Option = "[" Expression "]" .
Repetition = "{" Expression "}" .
```
这个基本语法列表其实不难理解。`=`定义了左侧这个标识所代表的含义,结尾的`.`表示一条规则定义的结束。其中的`|`符号表示可能性的组合,意为左侧的内容可能是被`|`组合的其中一项。`()`符号表示分组,`[]`表示可选项,即 0 或 1 的选择,`{}`表示循环,即反复出现 0 到 n 次。
例如上面这个定义中的`Syntax`就是由`Production`内容连续重复多次形成的。
定义中使用`""`包裹起来的内容都是字符串,是需要原封不动的用在语言中的。
这里从最基础的单元开始简单解释一下上面这个定义中的内容。
1. 最基础的单元是`Factor`,它可以是一下这么几种内容:定义规则名称(`production_name`)、标识序列(`token`或者`toekn ... token`)、分组内容、可选内容和重复内容。
1. `Term`是由至少一个`Factor`组成的。
1. `Expression`是由至少一个`Term`或者多个`Term`通过`|`符号连接起来的。
1. 分组内容就是用`()`包裹`Expression`。
1. 可选内容就是用`[]`包裹`Expression`。
1. 重复内容就是用`{}`包裹`Expression`。
1. 每一个规则定义`Production`都是使用`=`定义的,`=`左侧是规则的名称,右侧可以是`Expression`,整个定义使用`.`结束。
1. 整个语法规则`Syntax`是由若干的`Production`组合起来的。
以下借用 Go 中对于整型数值的定义来详细说明 ENBF 在 Go 语言规范定义中是如何使用的。
```
decimal_lit = "0" | ( "1" ... "9" ) [ [ "_" ] decimal_digits ] .
decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
decimal_digit = "0" ... "9" .
```
首先看其中第一条规则,`decimal_lit`按照规则,可以是`0`或者`1`到`9`的数字开头的,可以由`_`分隔的数字。而组成`decimal_lit`的`decimal_degits`则是由数字和`_`循环组成的。所以在 Go 中,一个整型数字可以是`0_382_3453_25`这样的结构。

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 775 KiB

View File

@ -1,51 +0,0 @@
# 创建一个新项目
在引入 Go Module 之前,创建一个 Go 语言项目是非常简单的事情,只需要在`$GOPATH/src`目录中新建一个目录就可以开始开发了。但是这种开发方式最令人诟病的就是无法控制依赖库的版本,因为`go get`命令会把项目中的依赖也下载到`$GOPATH/src`目录中,这样一来,如果两个项目引用了相同的依赖库,那么这个依赖库就有可能会因为新项目中运行的`go get`命令而升级,那之前的旧项目就惨了,很有可能这个依赖库发生了断代升级,直接导致旧项目需要强制更新。
在引入 Go Module 之后,这个问题得到了全面的改善。首先就是 Go 的项目不需要必须创建在`$GOPATH/src`目录里了,其次就是项目中的依赖也是可以得到比较有效的控制和管理了。
一个新的 Go 项目现在是通过`go mod`命令创建的,使用这个命令创建项目的时候需要注意,这个命令只会创建 Go 项目本身,并不会创建项目所在的目录。所以在执行`go mod`命令之前,我们需要首先自行创建项目所在的目录。这里需要记住的是,按照上面所说的,现在基于 Go Module 创建的 Go 项目已经不必放置在`$GOPATH/src`目录中了。
例如现在要创建一个电商网站服务端项目,就可以执行以下命令来完成创建。
```bash
mkdir ebusiness
cd ebusiness
go mod init archgrid.xyz/ebusiness
```
在`go mod init`命令后面所接的参数不是之前创建的目录名字,而是所要创建的这个项目的名字。这个名字可以随意起,但是需要注意的是,这个名字在习惯上跟项目所在的 Git 版本库的路径是一样的,例如项目所在的版本库路径是`https://github.com/example/ebusiness`,那么这个项目就最好起名为`github.com/example/ebusiness`,这样做的目的主要是为了方便 Go 在`go.mod`文件中记录项目的依赖,以及当前项目做为一个库被其他项目使用时,可以不必使用`replace`重新定义库所在的位置。
刚创建好的项目中一般只有`go.mod`一个文件。这个文件里的内容十分简单,如下所示。
```
module archgrid.xyz/ebusiness
go 1.20
```
现在这个`go.mod`文件只是一个最原始的状态。里面只是记录了当前这个项目的名称,这是由保留字`module`定义的。还定义了当前项目所依赖的 Go 版本,这是由保留字`go`来定义的,如果这个项目需要使用特定版本的 Go那么可以修改这里的定义。
在绝大多数情况下,`go.mod`文件的内容是不需要我们手动去编辑的,每次我们在执行`go get`和`go mod tidy`命令的时候,`go.mod`文件都会自动的被修改。当然这种情况也是有特例的,比如之前提到过的使用`replace`定义库的别名路径,这些用法会在后面介绍`go mod`命令时做更加详细的介绍。
现在就可以在这个项目的目录里新建一个`main.go`文件来编写 Go 项目的主启动文件了。
```admonish tip
`main.go`和`lib.go`都是Go项目中的习惯上对于主启动文件的命名你也可以使用其他的名称。
```
以下是一个最简单的`main.go`文件的内容,它只会做一件事情,就是在终端或者控制台上输出一个`Hello World!`。
```go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
```
这可以算是一个最小的 Go 语言程序。

View File

@ -1,15 +0,0 @@
# 项目基本结构
Go 语言项目的基本结构是非常自由的,基本上没有什么固定的要求或者规定可以遵守。
所以一般情况下我们会习惯性的按照功能或者按照业务属性对相关内容进行分类分目录放置。例如一个电商贸易类网站就可以像下图一样组织一套类似于传统 MVC 项目的结构。
![MVC模式项目结构](./mvc-mode.svg)
但是 Go 语言中的项目没有那么多的范式,同样的这个电商贸易类网站还可以用下面这样的面向业务的项目结构。
![面向业务的项目结构](./busi-mode.svg)
> 在这两个图示中,使用绿色标记的内容是文件夹,在 Go 中也是一个`package`。
以上两个图示中的项目都是带有`go.mod`文件的,这说明这个项目是使用`go mod`命令创建的,是使用了 Go Module 技术的项目。接下来就来看一下如何使用`go mod`命令来创建项目。

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 775 KiB

View File

@ -1,111 +0,0 @@
# 使用包来组织代码
Go 是通过包这个概念来组织项目中分属不同功能的代码的。其实在之前的最小示例中已经见识过了定义包的语句。在 Go 程序中,包是使用`package`关键词来定义的。
但是包的名称不是可以随意起的Go 在包的命名上有自己的一套规范。
1. 承载主启动函数的文件,其包名为`main`。
1. 同一个目录中的文件,包名的定义必须相同。
1. 除应用根目录以外,其他目录中的包名应该与目录名相同。例如目录名为`entities`,那么这个目录中的文件就需要使用`package entities`来声明其包名。
```admonish tip
一般习惯上把项目根目录命名为`main`包。而根目录通常也是放置主启动文件的地方,不建议把项目的主启动文件放置到其他目录中。项目的根目录中一般除了项目的主启动文件以外,一般也不建议再放置其他的功能文件,或者最多放置一些与项目启动相关的功能。
```
## 引用项目中定义的包
在一个代码文件中使用项目另一个包中定义的内容,就需要先引入这个包。在 Go 中,引用包是通过`import`关键字完成的。
其实`import`语句在之前的最小实例中也已经见到过了,在那个示例中是用来引入标准库中的`fmt`包的。这也是`import`关键字最简单的使用方法——`import "被引入包名"`。
在引入 Go 标准库中的包时,是可以直接书写包的名称的,比如`import "fmt"`、`import "io"`、`import "os"`。但是在引入项目中自定义的包或者是其他依赖库中的包时,就需要书写包的完整路径。例如`import "archgrid.xyz/ebusiness/config"`、`import "github.com/samber/lo"`。
在一个代码文件中通常不可能只会引入一个包而是会同时引入若干个包这是一个非常普遍的事情。与其他的语言不一样Go 不需要每一书写一个`import`来完成多个包的引用。例如以下这种引入方式实际上是不必要的。
```go
import "fmt"
import "os"
import "archgrid.xyz/ebusiness/config"
import "archgrid.xyz/ebusiness/global"
import "github.com/samber/lo"
```
事实上,如果真的在代码中写出了这样的引入语句,也会被`go fmt`命令自动优化掉。在 Go 代码文件中,这样引入若干个包的语句通常会被简化为一条使用括号包裹的`import`语句。也就是以下这样。
```go
import (
"fmt"
"os"
"archgrid.xyz/ebusiness/config"
"archgrid.xyz/ebusiness/global"
"github.com/samber/lo"
)
```
```admonish tip
在Go语言中这种使用括号来精简批量语句是一种使用非常普遍的语法范式在后面会介绍到还有哪些关键字可以使用这种语法范式。
```
在 Go 语言规范定义中`import`关键字的定义是下面这样的。
```
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec } ")") .
ImportSpec = [ "." | PackageName ] ImportPath .
ImportPath = string_lit .
```
## 引入的包在代码中的使用
一个包在使用`import`关键字引入之后就可以在当前代码文件中使用了,但是使用的时候需要记住的是,引入的内容需要通过包名来访问。
那这就有一个需要解决的问题了,一个被引入的包的包名是什么?
根据 Go 语言的语法规定,一个被引入的包,它的包名是它完整路径的最后一个部分。例如`"github.com/samber/lo"`的包名是`lo``"archgrid.xyz/ebusiness/config"`的包名是`config``"fmt"`的包名就还是`fmt`。此时根据包名就可以访问所引入包中的内容,例如可以使用`config.Service`来访问`archgrid.xyz/ebusiness/config`中声明的`Service`变量。
如果一个代码文件中引入的两个包的最后一个部分重名,那么就只能使用 Go 提供的给包重命名的方法来去分它们了。
```admonish caution
如果放任引入的包发生重名的问题,不仅会使程序无法辨识所使用的包成员,还会因此导致程序无法通过编译。
```
以下是一个对引入的重名包进行重命名的示例。
```go
import (
"io"
smithyIO "github.com/aws/smithy-go/io"
)
```
在这个示例中使用了一个新的语法:在被引入的包路径前面增加了一个标识符。这个新增加的标识符就是在当前文档中给这个被引入的包的命名。这样一来,在当前的文档中,在访问这个包的内容时,就不能像之前一样使用包路径的最后一个部分来做包名称了,而是要使用这个新增加的标识符。例如之前的用法`io.NewRingBuffer(buf)`,现在就要变成`smithyIO.NewRingBuffer(buf)`了。
对一个包进行重命名并不局限在包发生了重名的现象时,其实在任何时刻都可以对包进行重命名,条件只有一个——保证包在当前文件中的名称是唯一的。
### 几个特殊的用法
`_`在 Go 中做为一个特殊的标识符,大量的使用在程序的各个角落,这里来说它在引入包的时候的用法。在 Go 中,`_`通常的意义表示忽略,所以在引入包的情况下也是一样的。使用`_`来标识被引入的一个包,实际上并不会引入包内的任何内容,只会执行其中的`init()`函数完成包的初始化。这种操作在引入数据库驱动或者是一些框架的插件系统时非常有用,可以避免许多不必要的内容混入当前的代码文档中。
```admonish tip
关于`init()`函数将在后面讲述函数的章节进行更加详细的说明。
```
与`_`相反,使用`.`对被引入的包进行标识,可以将被引入的包中的内容完全引入当前的代码文档中,使之可以不通过包名即可使用,就像是这些内容就是在当前包中定义的一样。
```admonish caution
使用`.`标识被引入包的用法一般并不推荐,通常认为这种用法比较容易引起命名空间的混乱。
```
### 如何引入一个依赖库中的子级包
我们对于大部分常用依赖库中提供功能的了解都是通过集中存放 Go 依赖库文档的网站[Go PKG](https://pkg.go.dev)的。我们在浏览很多依赖库的时候都会发现它们在组织自身的代码的时候,也使用了大量的包。这就要求我们在使用的时候必须去引用其中的子级包。
比方说常用的`github.com/fufuok/utils`工具库,除了直接在`utils`包中提供的工具函数,还有大量的工具函数是在其子级包中提供的,例如提供加密算法支持的名为`xcrypto`子级包。这种子级包在使用的时候依旧需要遵照前面提到的原则,使用被引入包的绝对路径来引入,所以`xcrypto`包的引入语法就是`import github.com/fufuok/utils/xcrypto`。
```admonish tip
虽然在使用子级包之前已经通过`go get`安装了父级包的依赖,但是在使用子级包的时候,同样需要重新用`go get`安装一下子级包,虽然这并不会修改`go.mod`文件。不过目前市面上的大部分IDE都能够提示和处理这种情况。
一个依赖库中提供的子包可以在[Go PKG](https://pkg.go.dev)中依赖库文档的`Directories`部分中找到。
```

View File

@ -1,23 +0,0 @@
# 可见性
既然 Go 使用包来组织代码,就必然需要涉及到可见性的问题。可见性是在使用包组织代码时,控制包中的哪些内容在包外是可以通过包名引用直接使用的,哪些是仅可以在包内使用的。
Go 语言中的可见性控制规则其实特别的简单:如果哪个内容是允许在包外直接访问的,那么这个成员的名字只需要使用大写字母开头即可。例如函数`initiate()`就是一个包私有的内容,是不可以被包外的其他代码使用的,但是变量`ResultChan`就是一个可以直接在包外面使用的。
```admonish tip
这个可见性规则不仅在包的层级中适用,在后面介绍的结构体中也同样适用。
```
## 同一个包中不同文件之间的可见性
前面一节中提到同一个目录中的所有文件都必须使用同一个包名这就意味着他一个目录下的文件都是属于一个包的那么在这些文件之间Go 是如果控制可见性的?
答案可能与你预想的一样Go 既然把一个目录里的所有文件都作为一个包的内容,那么一个文件就是可以直接访问同一目录中其他文件中定义的内容的,无论被访问的内容是大写字母开头还是小写字母开头。这样带来的一个问题就是,一个代码文件可以直接访问其他文件中定义的内容,这实际上会给代码编辑和维护带来一定的困难,但是这并不是什么致命的问题,反而 Go 提供的这种特性更加方便把可以归类到一个包的功能拆分到若干更小的文件中去,避免了大量功能挤在一个大文件中的现象。
## 不同层级的包之间内容的可见性
在了解了 Go 对于一个包中内容可见性控制策略以后,那么就自然的会提出一个疑问:如果两个包是上下级的关系,那么它们之间的可见性控制策略是什么样的?
的确对于一些其他的语言来说,这个问题是需要特别记忆的,因为,有的语言是父级包可以直接访问子级包中的全部内容,有的是子级包可以访问父级包中的全部内容。但是对于 Go 来说,规则依旧非常简单,那就是继续坚持原有的可见性控制规则。
这个道理非常简单,也非常容易解释,那就是父级包和子级包都是 **独立存在** 的包,它们只是在硬盘上的逻辑存储结构上分出了上下级的关系而已。