|
|
@ -110,7 +110,7 @@
|
|
|
|
- gofmt,它统一了 Go 语言的代码风格,在其他语言开发者还在为代码风格争论不休的时候,Go 开发者可以更加专注于领域业务中。
|
|
|
|
- gofmt,它统一了 Go 语言的代码风格,在其他语言开发者还在为代码风格争论不休的时候,Go 开发者可以更加专注于领域业务中。
|
|
|
|
- 在提供丰富的工具链的同时,Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。
|
|
|
|
- 在提供丰富的工具链的同时,Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 基础
|
|
|
|
## 2. 环境
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 配好环境:选择一种最适合你的Go安装方法
|
|
|
|
### 2.1 配好环境:选择一种最适合你的Go安装方法
|
|
|
|
|
|
|
|
|
|
|
@ -267,12 +267,379 @@ hello, world
|
|
|
|
- 我们也可以使用 go mod tidy 命令,让 Go 工具自动添加第三方包
|
|
|
|
- 我们也可以使用 go mod tidy 命令,让 Go 工具自动添加第三方包
|
|
|
|
- go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。
|
|
|
|
- go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 2.3 标准先行:Go项目的布局标准是什么?
|
|
|
|
### 2.3 标准先行:Go项目的布局标准是什么?
|
|
|
|
- Go 语言“创世项目”结构是怎样的?
|
|
|
|
- Go 语言“创世项目”结构是怎样的?
|
|
|
|
- 用loccount 工具对 Go 语言发布的第一个Go 1.0 版本分析看看:
|
|
|
|
- 用loccount 工具对 Go 语言发布的第一个Go 1.0 版本分析看看:
|
|
|
|
- ![Go项目的布局标准](pic/Go项目的布局标准.png)
|
|
|
|
- ![Go项目的布局标准](pic/Go项目的布局标准.png)
|
|
|
|
|
|
|
|
- 现在的 Go 项目的典型结构布局是怎样的?
|
|
|
|
|
|
|
|
- 可执行程序项目是以构建可执行程序为目的的项目,Go 社区针对这类 Go 项目所形成的典型结构布局是这样的:
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
|
|
|
$tree -F exe-layout
|
|
|
|
|
|
|
|
exe-layout
|
|
|
|
|
|
|
|
├── cmd/
|
|
|
|
|
|
|
|
│ ├── app1/
|
|
|
|
|
|
|
|
│ │ └── main.go
|
|
|
|
|
|
|
|
│ └── app2/
|
|
|
|
|
|
|
|
│ └── main.go
|
|
|
|
|
|
|
|
├── go.mod
|
|
|
|
|
|
|
|
├── go.sum
|
|
|
|
|
|
|
|
├── internal/
|
|
|
|
|
|
|
|
│ ├── pkga/
|
|
|
|
|
|
|
|
│ │ └── pkg_a.go
|
|
|
|
|
|
|
|
│ └── pkgb/
|
|
|
|
|
|
|
|
│ └── pkg_b.go
|
|
|
|
|
|
|
|
├── pkg1/
|
|
|
|
|
|
|
|
│ └── pkg1.go
|
|
|
|
|
|
|
|
├── pkg2/
|
|
|
|
|
|
|
|
│ └── pkg2.go
|
|
|
|
|
|
|
|
└── vendor/
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 先来看 cmd 目录。cmd 目录就是存放项目要编译构建的可执行文件对应的 main 包的源文件。如果你的项目中有多个可执行文件需要构建,每个可执行
|
|
|
|
|
|
|
|
文件的 main 包单独放在一个子目录中,比如图中的 app1、app2,cmd 目录下的各 app 的 main 包将整个项目的依赖连接在一起。
|
|
|
|
|
|
|
|
- 而且通常来说,main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高
|
|
|
|
|
|
|
|
级的执行控制对象。另外,也有一些 Go 项目将 cmd 这个名字改为 app 或其他名字,但它的功能其实并没有变。
|
|
|
|
|
|
|
|
- 接着我们来看 pkgN 目录,这是一个存放项目自身要使用、同样也是可执行文件对应 main 包所要依赖的库文件,同时这些目录下的包还可以被外部项目引用。
|
|
|
|
|
|
|
|
- 然后是 go.mod 和 go.sum,它们是 Go 语言包依赖管理使用的配置文件。我们前面说过,Go 1.11 版本引入了 Go Module 构建机制,这里我建议你所有新项目都基于 Go
|
|
|
|
|
|
|
|
Module 来进行包依赖管理,因为这是目前 Go 官方推荐的标准构建模式
|
|
|
|
|
|
|
|
- 对于还没有使用 Go Module 进行包依赖管理的遗留项目,比如之前采用 dep、glide 等作为包依赖管理工具的,建议尽快迁移到 Go Module 模式。Go 命令支持直接将 dep 的
|
|
|
|
|
|
|
|
Gopkg.toml/Gopkg.lock 或 glide 的 glide.yaml/glide.lock 转换为 go.mod。
|
|
|
|
|
|
|
|
- 最后我们再来看看 vendor 目录。vendor 是 Go 1.5 版本引入的用于在项目本地缓存特定版本依赖包的机制,在 Go Modules 机制引入前,基于 vendor 可以实现可重现构建,保
|
|
|
|
|
|
|
|
证基于同一源码构建出的可执行程序是等价的。
|
|
|
|
|
|
|
|
- 不过呢,我们这里将 vendor 目录视为一个可选目录。原因在于,Go Module 本身就支持可再现构建,而无需使用 vendor。 当然 Go Module 机制也保留了 vendor 目录(通过
|
|
|
|
|
|
|
|
go mod vendor 可以生成 vendor 下的依赖包,通过 go build -mod=vendor 可以实现基于 vendor 的构建)。一般我们仅保留项目根目录下的 vendor 目录,
|
|
|
|
|
|
|
|
否则会造成不必要的依赖选择的复杂性。
|
|
|
|
|
|
|
|
- 当然了,有些开发者喜欢借助一些第三方的构建工具辅助构建,比如:make、bazel 等。你可以将这类外部辅助构建工具涉及的诸多脚本文件(比如 Makefile)放置在项目的顶层
|
|
|
|
|
|
|
|
目录下,就像 Go 创世项目中的 all.bash 那样。
|
|
|
|
|
|
|
|
- 当然如果你非要在一个代码仓库中存放多个 module,那么新版 Go 命令也提供了很好的支持。比如下面代码仓库 multi-modules 下面有三个 module:mainmodule、
|
|
|
|
|
|
|
|
module1 和 module2:
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
|
|
|
$tree multi-modules
|
|
|
|
|
|
|
|
multi-modules
|
|
|
|
|
|
|
|
├── go.mod // mainmodule
|
|
|
|
|
|
|
|
├── module1
|
|
|
|
|
|
|
|
│ └── go.mod // module1
|
|
|
|
|
|
|
|
└── module2
|
|
|
|
|
|
|
|
└── go.mod // module2
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 我们可以通过 git tag 名字来区分不同 module 的版本。其中 vX.Y.Z 形式的 tag 名字用于代码仓库下的 mainmodule;而 module1/vX.Y.Z 形式的 tag 名字用于指示 module1 的
|
|
|
|
|
|
|
|
版本;同理,module2/vX.Y.Z 形式的 tag 名字用于指示 module2 版本。
|
|
|
|
|
|
|
|
- 如果 Go 可执行程序项目有一个且只有一个可执行程序要构建,那就比较好办了,我们可以将上面项目布局进行简化:
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
|
|
|
$tree -F -L 1 single-exe-layout
|
|
|
|
|
|
|
|
single-exe-layout
|
|
|
|
|
|
|
|
├── go.mod
|
|
|
|
|
|
|
|
├── internal/
|
|
|
|
|
|
|
|
├── main.go
|
|
|
|
|
|
|
|
├── pkg1/
|
|
|
|
|
|
|
|
├── pkg2/
|
|
|
|
|
|
|
|
└── vendor/
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 你可以看到,我们删除了 cmd 目录,将唯一的可执行程序的 main 包就放置在项目根目录下,而其他布局元素的功用不变。
|
|
|
|
|
|
|
|
- 好了到这里,我们已经了解了 Go 可执行程序项目的典型布局,现在我们再来看看 **Go 库项目**的典型结构布局是怎样的。
|
|
|
|
|
|
|
|
- Go 库项目仅对外暴露 Go 包,这类项目的典型布局形式是这样的:
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
|
|
|
$tree -F lib-layout
|
|
|
|
|
|
|
|
lib-layout
|
|
|
|
|
|
|
|
├── go.mod
|
|
|
|
|
|
|
|
├── internal/
|
|
|
|
|
|
|
|
│ ├── pkga/
|
|
|
|
|
|
|
|
│ │ └── pkg_a.go
|
|
|
|
|
|
|
|
│ └── pkgb/
|
|
|
|
|
|
|
|
│ └── pkg_b.go
|
|
|
|
|
|
|
|
├── pkg1/
|
|
|
|
|
|
|
|
│ └── pkg1.go
|
|
|
|
|
|
|
|
└──pkg2/
|
|
|
|
|
|
|
|
└──pkg2.go
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- Go 库项目的初衷是为了对外部(开源或组织内部公开)暴露 API,对于仅限项目内部使用而不想暴露到外部的包,可以放在项目顶层的 internal 目录下面。当然 internal 也可以有
|
|
|
|
|
|
|
|
多个并存在于项目结构中的任一目录层级中,关键是项目结构设计人员要明确各级 internal 包的应用层次和范围。
|
|
|
|
|
|
|
|
- 对于有一个且仅有一个包的 Go 库项目来说,我们也可以将上面的布局做进一步简化,简化的布局如下所示:
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
|
|
|
$tree -L 1 -F single-pkg-lib-layout
|
|
|
|
|
|
|
|
single-pkg-lib-layout
|
|
|
|
|
|
|
|
├── feature1.go
|
|
|
|
|
|
|
|
├── feature2.go
|
|
|
|
|
|
|
|
├── go.mod
|
|
|
|
|
|
|
|
└──internal/
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 简化后,我们将这唯一包的所有源文件放置在项目的顶层目录下(比如上面的 feature1.go和 feature2.go),其他布局元素位置和功用不变。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 2.4 构建模式:Go是怎么解决包依赖管理问题的?
|
|
|
|
|
|
|
|
- Go 构建模式是怎么演化的?
|
|
|
|
|
|
|
|
- Go 程序由 Go 包组合而成的,Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程。
|
|
|
|
|
|
|
|
- Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH、1.5 版本的 Vendor 机制,以及现在的 Go Module。
|
|
|
|
|
|
|
|
- 首先我们来看 GOPATH
|
|
|
|
|
|
|
|
- Go 编译器可以在本地 GOPATH 环境变量配置的路径下,搜寻 Go 程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误。
|
|
|
|
|
|
|
|
- GOPATH 构建模式下编写的代码
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
logrus.Println("hello, gopath mode")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 你可以看到,这段代码依赖了第三方包 logrus(logrus 是 Go 社区使用最为广泛的第三方 log 包)。
|
|
|
|
|
|
|
|
- Go 编译器在 GOPATH 构建模式下,究竟怎么在 GOPATH 配置的路径下搜寻第三方依赖包呢?
|
|
|
|
|
|
|
|
- 可以通过 go get 命令将本地缺失的第三方依赖包下载到本地
|
|
|
|
|
|
|
|
```shell
|
|
|
|
|
|
|
|
$go get github.com/sirupsen/logrus
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 这里的 go get 命令,不仅能将 logrus 包下载到 GOPATH 环境变量配置的目录下,它还会检查 logrus 的依赖包在本地是否存在,如果不存在,go get 也会一并将它们下载到本地。
|
|
|
|
|
|
|
|
- 也就是说,在 GOPATH 构建模式下,Go 编译器实质上并没有关注 Go 项目所依赖的第三方包的版本。但 Go 开发者希望自己的 Go 项目所依赖的第三方包版本能受到自己的控
|
|
|
|
|
|
|
|
制,而不是随意变化。于是 Go 核心开发团队引入了 Vendor 机制试图解决上面的问题。
|
|
|
|
|
|
|
|
- 要想开启 vendor 机制,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。如果不满足这一路径要求,那么 Go 编译器是不会理会
|
|
|
|
|
|
|
|
Go 项目目录下的 vendor 目录的。
|
|
|
|
|
|
|
|
- 就在 Go 社区为包依赖管理焦虑并抱怨没有官方工具的时候,Go 核心团队基于社区实践的经验和教训,推出了 Go 官方的解决方案:Go Module。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 创建你的第一个 Go Module
|
|
|
|
|
|
|
|
- 在 Go Module 模式下,通常一个代码仓库对应一个 Go Module。一个 Go Module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module,也就是
|
|
|
|
|
|
|
|
说 Go Module 与 go.mod 是一一对应的。
|
|
|
|
|
|
|
|
- go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **创建一个 Go Module**
|
|
|
|
|
|
|
|
- 第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
|
|
|
|
|
|
|
|
- 第二步,通过 go mod tidy 命令自动更新当前 module 的依赖信息;
|
|
|
|
|
|
|
|
- 第三步,执行 go build,执行新 module 的构建。
|
|
|
|
-
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 语法
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 **变量声明** :静态语言有别于动态语言的重要特征
|
|
|
|
|
|
|
|
- 在编程语言中,为了方便操作内存特定位置的数据,我们**用一个特定的名字与位于特定位置的内存块绑定在一起**,这个名字被称为**变量**。
|
|
|
|
|
|
|
|
- 但这并不代表我们可以通过变量随意引用或修改内存,变量所绑定的内存区域是要有**一个明确的边界的**。
|
|
|
|
|
|
|
|
- 动态语言(比如 Python、Ruby 等)的解释器可以在运行时通过对变量赋值的分析,**自动确定变量的边界**。并且在动态语言中,一个变量可以在运行时被赋予大小不同的边界。
|
|
|
|
|
|
|
|
- Go 语言,它沿袭了静态语言的这一要求:**使用变量之前需要先进行变量声明**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- Go 语言的变量声明方法
|
|
|
|
|
|
|
|
- 通用的变量声明方法
|
|
|
|
|
|
|
|
- ![通用的变量声明方法](pic/通用的变量声明方法.png)
|
|
|
|
|
|
|
|
- 这个变量声明分为四个部分:
|
|
|
|
|
|
|
|
- var 是修饰变量声明的关键字;
|
|
|
|
|
|
|
|
- a 为变量名;
|
|
|
|
|
|
|
|
- int 为该变量的类型;
|
|
|
|
|
|
|
|
- 10 是变量的初值。
|
|
|
|
|
|
|
|
- 其实 Go 语言的变量声明形式与其他主流静态语言有一个显著的差异,那就是它将**变量名放在了类型的前面**
|
|
|
|
|
|
|
|
- 如果你没有**显式**为变量赋予初值,Go 编译器会为变量**赋予这个类型的零值**:
|
|
|
|
|
|
|
|
- var a int // a的初值为int类型的零值:0
|
|
|
|
|
|
|
|
- Go 语言的每种原生类型都有它的默认值,这个默认值就是这个类型的零值。
|
|
|
|
|
|
|
|
- Go 规范定义的内置原生类型的默认值(即零值)
|
|
|
|
|
|
|
|
- ![内置原生类型的默认值](pic/内置原生类型的默认值.png)
|
|
|
|
|
|
|
|
- 像数组、结构体这样复合类型变量的零值就是它们**组成元素都为零值时的结果**
|
|
|
|
|
|
|
|
- 除了单独声明每个变量外,Go 语言还提供了变量声明块(block)的语法形式,可以**用一个 var 关键字将多个变量声明放在一起**,像下面代码这样:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
a int = 128
|
|
|
|
|
|
|
|
b int8 = 6
|
|
|
|
|
|
|
|
s string = "hello"
|
|
|
|
|
|
|
|
c rune = 'A'
|
|
|
|
|
|
|
|
t bool = true
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 而且,Go 语言还支持在**一行变量声明中同时声明多个变量**:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var a, b, c int = 5, 6, 7
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 这样的多变量声明同样也可以用在变量声明块中,像下面这样:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
a, b, c int = 5, 6, 7
|
|
|
|
|
|
|
|
c, d, e rune = 'C', 'D', 'E'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 当然了,虽然我们现在写的多变量声明都是在声明同一类型的变量,**但是它也适用于声明不同类型的变量**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 为了给开发者带来更好的使用体验,Go 语言还提供了两种变量声明的 **“语法糖”**
|
|
|
|
|
|
|
|
- 省略类型信息的声明:
|
|
|
|
|
|
|
|
- 在通用的变量声明的基础上,Go 编译器允许我们省略变量声明中的类型信息,它的标准范式是 **“var varName = initExpression”**, 省略了类型信息的变量声明:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var b = 13
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- Go 编译器**会根据右侧变量初值自动推导出变量的类型**,并给这个变量赋予初值所对应的默认类型。比如,整型值的默认类型 int,浮点值的默认类型为 float64,复数
|
|
|
|
|
|
|
|
值的默认类型为 complex128。其他类型值的默认类型就更好分辨了,在 Go 语言中仅有唯一与之对应的类型,比如布尔值的默认类型只能是 bool,字符值默认类型只能是
|
|
|
|
|
|
|
|
true,字符串值的默认类型只能是 string 等。
|
|
|
|
|
|
|
|
- 如果我们不接受默认类型,而是要显式地为变量指定类型,除了通用的声明形式,我们还可以通过**显式类型转型**达到我们的目的:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var b = int32(13)
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 显然这种省略类型信息声明的“语法糖”仅**适用于在变量声明的同时显式赋予变量初值的情况**
|
|
|
|
|
|
|
|
- 结合多变量声明,我们可以使用这种变量声明“语法糖”**声明多个不同类型的变量**:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var a, b, c = 12, 'A', "hello"
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 我们声明了三个变量 a、b 和 c,但它们分别具有不同的类型,分别为 int、rune 和 string。
|
|
|
|
|
|
|
|
- 是否还有更简化的变量声明形式呢?答案是有的。下面我们就来看看短变量声明。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 短变量声明:
|
|
|
|
|
|
|
|
- 使用短变量声明时,我们甚至可以省去 var 关键字以及类型信息,它的标准范式是 **“varName := initExpression”**
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
a := 12
|
|
|
|
|
|
|
|
b := 'A'
|
|
|
|
|
|
|
|
c := "hello"
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 而且,短变量声明也支持一次声明多个变量,而且形式更为简洁,是这个样子的:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
a, b, c := 12, 'A', "hello"
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- Go 语言的两类变量
|
|
|
|
|
|
|
|
- Go 语言的变量可以分为两类:
|
|
|
|
|
|
|
|
- 一类称为包级变量 (package varible),也就是在包级别可见的变量。
|
|
|
|
|
|
|
|
- 如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量;
|
|
|
|
|
|
|
|
- 另一类则是局部变量 (local varible),也就是 Go 函数或方法体内声明的变量,仅在函数或方法体内可见。
|
|
|
|
|
|
|
|
- 而我们声明的所有变量都逃不开这两种。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 包级变量的声明形式
|
|
|
|
|
|
|
|
- 先下个结论:**包级变量只能使用带有 var 关键字的变量声明形式**,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。
|
|
|
|
|
|
|
|
- 我们可以从 **“变量声明时是否延迟初始化”** 这个角度,对包级变量的声明形式进行一次分类。
|
|
|
|
|
|
|
|
- **第一类:声明并同时显式初始化。**
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
// $GOROOT/src/io/io.go
|
|
|
|
|
|
|
|
var ErrShortWrite = errors.New("short write")
|
|
|
|
|
|
|
|
var ErrShortBuffer = errors.New("short buffer")
|
|
|
|
|
|
|
|
var EOF = errors.New("EOF")
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 这个代码块里声明的变量都是 io 包的包级变量。在 Go 标准库中,对于变量声明的同时进行显式初始化的这类包级变量,实践中多使用这种省略类型信息的“语法糖”格式:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var varName = initExpression
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- Go 编译器会自动根据等号右侧 InitExpression 结果值的类型,来确定左侧声明的变量的类型,这个类型会是结果值对应类型的默认类型。
|
|
|
|
|
|
|
|
- 当然,如果我们不接受默认类型,而是要显式地为包级变量指定类型,那么我们有两种方式,我这里给出了**两种包级变量的声明形式**的对比示例。
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
//第一种:
|
|
|
|
|
|
|
|
plain
|
|
|
|
|
|
|
|
var a = 13 // 使用默认类型
|
|
|
|
|
|
|
|
var b int32 = 17 // 显式指定类型
|
|
|
|
|
|
|
|
var f float32 = 3.14 // 显式指定类型
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//第二种:
|
|
|
|
|
|
|
|
var a = 13 // 使用默认类型
|
|
|
|
|
|
|
|
var b = int32(17) // 显式指定类型
|
|
|
|
|
|
|
|
var f = float32(3.14) // 显式指定类型
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 虽然这两种方式都是可以使用的,但从声明一致性的角度出发,Go 更推荐我们**使用后者**,这样能统一接受默认类型和显式指定类型这两种声明形式,尤其是在将这些变量放在一个
|
|
|
|
|
|
|
|
var 块中声明时,你会更明显地看到这一点。
|
|
|
|
|
|
|
|
- 所以我们更青睐下面这样的形式:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
a = 13
|
|
|
|
|
|
|
|
b = int32(17)
|
|
|
|
|
|
|
|
f = float32(3.14)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- **第二类:声明但延迟初始化。**
|
|
|
|
|
|
|
|
- 对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var a int32
|
|
|
|
|
|
|
|
var f float64
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 我们知道,虽然没有显式初始化,Go 语言也会让这些变量拥有初始的“零值”。如果是自定义的类型,我也建议你**尽量保证它的零值是可用的**。
|
|
|
|
|
|
|
|
- 这里还有一个注意事项,就是**声明聚类与就近原则**。
|
|
|
|
|
|
|
|
- 正好,Go 语言提供了**变量声明块**用来把多个的变量声明放在一起,并且在语法上也不会限制放置在 var 块中的声明类型,那我们就应该学会充分利用 var 变量声明块,让我们变量
|
|
|
|
|
|
|
|
声明更规整,更具可读性,现在我们就来试试看。
|
|
|
|
|
|
|
|
- 其实,我们**可以将延迟初始化的变量声明放在一个 var 声明块** (比如上面的第一个 var 声明块),然后将声明且**显式初始化的变量放在另一个 var 块中**(比如上面的第二个 var 声明
|
|
|
|
|
|
|
|
块),这里我称这种方式为“声明聚类”,声明聚类可以提升代码可读性。
|
|
|
|
|
|
|
|
- 我们是否应该将包级变量的声明全部集中放在源文件头部呢?答案不能一概而论。
|
|
|
|
|
|
|
|
- 使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:**就近原则**。
|
|
|
|
|
|
|
|
- 尽可能在靠近第一次使用变量的位置声明这个变量,就近原则实际上也是对变量的作用域最小化的一种实现手段。
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
// $GOROOT/src/net/http/request.go
|
|
|
|
|
|
|
|
var ErrNoCookie = errors.New("http: named cookie not present")
|
|
|
|
|
|
|
|
func (r *Request) Cookie(name string) (*Cookie, error) {
|
|
|
|
|
|
|
|
for _, c := range readCookies(r.Header, name) {
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
|
|
|
|
}r
|
|
|
|
|
|
|
|
eturn nil, ErrNoCookie
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 在这个代码块里,ErrNoCookie 这个变量在整个包中仅仅被用在了 Cookie 方法中,因此它被声明在紧邻 Cookie 方法定义的地方。当然了,如果一个包级变量在**包内部被多处**使
|
|
|
|
|
|
|
|
用,那么这个变量还是放在**源文件头部**声明比较适合的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **局部变量的声明形式**
|
|
|
|
|
|
|
|
- 这里我们也从“变量声明的时候是否延迟初始化”这个角度,对本地变量的声明形式进行分类说明。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 第一类:**对于延迟初始化的局部变量声明,我们采用通用的变量声明形式**
|
|
|
|
|
|
|
|
- 省略类型信息的声明和短变量声明这两种“语法糖”变量声明形式都不支持变量的延迟初始化,因此对于这类局部变量,和包级变量一样,我们只能采用通用的变量声明形式:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 第二类:**对于声明且显式初始化的局部变量,建议使用短变量声明形式**
|
|
|
|
|
|
|
|
- 短变量声明形式是局部变量最常用的声明形式,它遍布在 Go 标准库代码中。对于接受默认类型的变量,我们使用下面这种形式:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
a := 17
|
|
|
|
|
|
|
|
f := 3.14
|
|
|
|
|
|
|
|
s := "hello, gopher!"
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在":="右侧要做一个显式转型,以保持声明的一致性:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
a := int32(17)
|
|
|
|
|
|
|
|
f := float32(3.14)
|
|
|
|
|
|
|
|
s := []byte("hello, gopher!")
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 这里我们还要注意:**尽量在分支控制时使用短变量声明形式**。
|
|
|
|
|
|
|
|
- strings 包的 LastIndexAny 方法为我们很好地诠释了如何**将短变量声明形式与分支控制语句融合在一起使用**:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
// $GOROOT/src/strings/strings.go
|
|
|
|
|
|
|
|
func LastIndexAny(s, chars string) int {
|
|
|
|
|
|
|
|
if chars == "" {
|
|
|
|
|
|
|
|
// Avoid scanning all of s.
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(s) > 8 {
|
|
|
|
|
|
|
|
// 作者注:在if条件控制语句中使用短变量声明形式声明了if代码块中要使用的变量 as和 isASCII
|
|
|
|
|
|
|
|
if as, isASCII := makeASCIISet(chars); isASCII {
|
|
|
|
|
|
|
|
for i := len(s) - 1; i >= 0; i-- {
|
|
|
|
|
|
|
|
if as.contains(s[i]) {
|
|
|
|
|
|
|
|
return i
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := len(s); i > 0; {
|
|
|
|
|
|
|
|
// 作者注:在for循环控制语句中使用短变量声明形式声明了for代码块中要使用的变量c
|
|
|
|
|
|
|
|
r, size := utf8.DecodeLastRuneInString(s[:i])
|
|
|
|
|
|
|
|
i -= size
|
|
|
|
|
|
|
|
for _, c := range chars {
|
|
|
|
|
|
|
|
if r == c {
|
|
|
|
|
|
|
|
return i
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 而且,短变量声明的这种融合的使用方式也体现出“就近”原则,**让变量的作用域最小化**。
|
|
|
|
|
|
|
|
- 但是如果你在声明局部变量时遇到了适合聚类的应用场景,你也应该毫不犹豫地使用 var 声明块来声明多于一个的局部变量,具体写法你可以参考 Go 标准库 net 包中 resolveAddrList 方法:
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
|
|
// $GOROOT/src/net/dial.go
|
|
|
|
|
|
|
|
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
|
|
|
|
|
|
|
|
... ...
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
|
|
tcp *TCPAddr
|
|
|
|
|
|
|
|
udp *UDPAddr
|
|
|
|
|
|
|
|
ip *IPAddr
|
|
|
|
|
|
|
|
wildcard bool
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
... ...
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
- 总结:
|
|
|
|
|
|
|
|
- ![变量声明方式总结](pic/变量声明方式总结.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 代码块与作用域:如何保证变量不会被遮蔽?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -282,6 +649,7 @@ hello, world
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3
|
|
|
|
|
|
|
|
|
|
|
|
## 并发
|
|
|
|
## 并发
|
|
|
|
|
|
|
|
|
|
|
|