You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

666 lines
44 KiB
Markdown

# Go
## 0. 目录
## 1. 背景
### 1.1 历史和现状
- Go 语言是怎样诞生的?
- Go 语言的创始人有三位分别是图灵奖获得者、C 语法联合发明人、Unix 之父**肯·汤普森 Ken Thompson**、Plan 9 操作系统领导者、UTF-8 编码的最初设计者**罗伯·派克Rob Pike**
,以及 Java 的 HotSpot 虚拟机和 Chrome 浏览器的 JavaScript V8 引擎的设计者之一**罗伯特·格瑞史莫Robert Griesemer**
- ![Go语言之父们](pic/Go语言之父们.png)
- Go语言第一版特性设计稿
- ![Go语言第一版特性设计稿](pic/Go语言第一版特性设计稿.png)
- 主要思路是,在 C 语言的基础上, **修正一些明显的缺陷,删除一些被诟病较多的特性,增加一些缺失的功能**
- 很多 Go 语言初学者经常称这门语言为 golang其实这是不对的“golang”仅应用于命名 Go 语言官方网站,而且当时没有用 go.com 纯粹是**这个域名被占用了**而已
- 在 Go 语言项目开源后Go 语言也迎来了自己的“吉祥物”,是一只由罗伯·派克夫人**芮妮·弗伦奇Renee French**设计的地鼠从此地鼠gopher也就成为了世界各地 Go
程序员的象征Go 程序员也被昵称为 **Gopher**
- ![Go语言的吉祥物](pic/Go语言的吉祥物.png)
- 2012 年 3 月 28 日Go 1.0 版本正式发布
- 只要符合 Go1 语言规范的源代码Go 编译器将保证向后兼容backwards compatible也就是说我们使用**新版编译器也可以正确编译用老版本语法编写的代码**。
- Go语言大事记
- ![Go语言大事记](pic/Go语言大事记.png)
- Go 是否值得我们学习?
- Go 语言已经逐渐成为了**云计算时代基础设施的编程语言**
- Docker、Kubernetes、Prometheus、Ethereum以太坊、Istio、CockroachDB、InfluxDB、Terraform、Etcd、Consul
- Gartner 的技术成熟度曲线The Hype Cycle
- Gartner 的技术成熟度曲线又叫技术循环曲线,是企业用来评估新科技是否要采用或采用时机的一种可视化方法,它利用时间轴与该技术在市面上的可见度(媒体曝光度)决定要
不要采用,以及什么时候采用这种新科技
- ![Gartner技术成熟度曲线](pic/Gartner技术成熟度曲线.png)
- ![Go语言的技术成熟度曲线](pic/Go语言的技术成熟度曲线.png)
- 预计在 2022 年初发布的支持 Go 泛型特性的 Go 1.18 版本,会是继 Go 1.5 版本之后又一“爆款”,很可能会快速推动 Go 迈入更高的发展阶段。
### 1.2 拒绝“Hello and Bye”Go语言的设计哲学是怎么一回事
- 根据“绝大多数主流编 程语言将在其 15 至 20 年间大步前进”这个依据我们给出了一个结论Go 语言即将进入自己的**黄金 5~10 年**。
- 所谓编程语言的设计哲学,就是指决定这门语言演化进程的高级原则和依据。
- Go 语言的设计哲学总结为五点:简单、显式、组合、并发和面向工程。
- **简单**
- Go 语言的设计者们在语言设计之初,就拒绝了走语言特性融合的道路,选择了“做减法”并致力于打造一门**简单的编程语言**
- 复杂性被 Go 语言的设计者们“隐藏”了,所以 Go 语法层面上呈现了这样的状态:
- 仅有 25 个关键字,主流编程语言最少;
- 内置垃圾收集,降低开发人员内存管理的心智负担;
- 首字母大小写决定可见性,无需通过额外关键字修饰;
- 变量初始为类型零值,避免以随机值作为初值的问题;
- 内置数组边界检查,极大减少越界访问带来的安全隐患;
- 内置并发支持,简化并发程序设计;
- 内置接口类型,为组合的设计哲学奠定基础;
- 原生提供完善的工具链,开箱即用;
- Go 设计者选择的“简单”,其实是站在巨人肩膀上,去除或优化了以往语言中,已经被开发者证明为体验不好或难以驾驭的语法元素和语言机制,并提出了自己的一些创新性的设
计。比如,首字母大小写决定可见性、变量初始为类型零值、内置以 go 关键字实现的并发支持等
- **显式**
- Go 不允许不同类型的整型变量进行混合计算,它同样也不会对其进行隐式的自动转换。
- 在 Go 语言中,不同类型变量是不能在一起进行混合计算的,这是因为 Go 希望**开发人员明确知道自己在做什么**,这与 C 语言的“信任程序员”原则完全不同,因此你需要以显式
的方式通过转型统一参与计算各个变量的类型。
- 除此之外Go 设计者所崇尚的显式哲学还直接决定了 Go 语言错误处理的形态Go 语言采用了**显式的基于值比较的错误处理方案**,函数 / 方法中的错误都会通过 return 语句显式
地返回,并且通常调用者不能忽略对返回的错误的处理。
- **组合**
- Go 语言不像 C++、Java 等主流面向对象语言,我们在 Go 中是找不到经典的面向对象语法元素、类型体系和继承机制的Go 推崇的是**组合**的设计哲学。
- 在 Go 语言设计层面Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:
- Go 语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念;
- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
- 实现某个接口时,无需像 Java 那样采用特定关键字修饰;
- 包之间是相对独立的,没有子包的概念。
- 无论是包、接口还是一个个具体的类型定义Go 语言其实是为我们呈现了这样的一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。那么现在摆在面
前的工作,就是在这些孤岛之间以最适当的方式建立关联,并形成一个整体。而 **Go 选择采用的组合方式,也是最主要的方式**
- Go 语言为支撑组合的设计提供了类型嵌入Type Embedding。通过类型嵌入我们可以将**已经实现的功能嵌入到新类型中**,以快速满足新类型的功能需求,这种方式有些类似
经典面向对象语言中的“继承”机制,但在原理上却与面向对象中的继承完全不同,这是一种 Go 设计者们精心设计的“语法糖”。
- 被嵌入的类型和新类型两者之间没有任何关系甚至相互完全不知道对方的存在更没有经典面向对象语言中的那种父类、子类的关系以及向上、向下转型Type Casting
通过新类型实例调用方法时,方法的匹配主要取决于方法名字,而不是类型。这种组合方式,我称之为垂直组合,即**通过类型嵌入,快速让一个新类型“复用”其他类型已经实现**
**的能力**,实现功能的垂直扩展。
- 垂直组合本质上是一种“能力继承”采用嵌入方式定义的新类型继承了嵌入类型的能力。Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是
一种**能力委托Delegate**,我们通常使用**接口类型**来实现水平组合。
- Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部
分之间“**纽带**”。
- 水平组合的模式有很多,比如一种常见方法就是,通过接受接口类型参数的普通函数进行组合
- func ReadAll(r io.Reader)([]byte, error) // $GOROOT/src/io/ioutil/ioutil.go
- func Copy(dst Writer, src Reader)(written int64, err error) // $GOROOT/src/io/io.go
- 还可以将 Go 语言内置的并发能力进行灵活组合以实现比如通过goroutine+channel 的组合,可以实现类似 Unix Pipe 的能力。
- 组合也让遵循“简单”原则的 Go 语言,在表现力上丝毫不逊色于其他复杂的主流编程语言。
- **并发**
- 并发”这个设计哲学的出现有它的背景,你也知道 CPU 都是靠**提高主频**来改进性能的,但是现在这个做法已经遇到了瓶颈。主频提高导致 CPU 的功耗和发热量剧增,反过来制约
了 CPU 性能的进一步提高。2007 年开始,处理器厂商的竞争焦点**从主频转向了多核**。
- 在这种大背景下Go 的设计者在决定去创建一门新语言的时候,果断将面向**多核、原生支持并发**作为了新语言的设计原则之一。并且Go 放弃了传统的基于操作系统线程的并发模
而采用了用户层轻量级线程Go 将之称为 **goroutine**
- goroutine 占用的资源非常小Go 运行时默认为每个 goroutine 分配的栈空间仅 **2KB**。goroutine 调度的切换也不用陷入trap操作系统内核层完成代价很低。因此一个
Go 程序中可以创建成千上万个并发的 goroutine。而且所有的 Go 代码都在 goroutine 中执行,哪怕是 go 运行时的代码也不例外。
- 在提供了开销较低的 goroutine 的同时Go 还在语言层面内置了辅助并发设计的原语:**channel 和 select**。开发者可以通过语言内置的 channel 传递消息或实现同步,并通过
select 实现多路 channel 的并发控制。相较于传统复杂的线程并发模型Go 对并发的原生支持将大大降低开发人员在开发并发程序时的心智负担。
- 此外,并发的设计哲学不仅仅让 Go 在语法层面提供了并发原语支持,其对 Go 应用程序设计的影响更为重要。并发是一种程序结构设计的方法,它使得并行成为可能
- 采用并发方案设计的程序在单核处理器上也是可以正常运行的,也许在单核上的处理性能可能不如非并发方案。但随着处理器核数的增多,并发方案可以自然地提高处理性能
- 而且并发与组合的哲学是一脉相承的并发是一个更大的组合的概念它在程序设计的全局层面对程序进行拆解组合再映射到程序执行层面上goroutines 各自执行特定的工
作,通过 channel+select 将 goroutines 组合连接起来。并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让 Go 语言也更适应现代计算环境
- **面向工程**
- Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理
解、跨语言构建难等
- 在 Go 语言最初设计阶段就将解决工程问题作为 Go 的设计原则之一去考虑 Go 语法、工具链与标准库的设计,这也是 Go 与其他偏学院派、偏
研究型的编程语言在设计思路上的一个重大差异
- 在面向工程设计哲学的驱使下Go 在语法设计细节上做了精心的打磨。比如:
- 重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度;
- 如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间;
- 去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建;
- 包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。“包名称不必是唯一的”这个约定,大大降低了开发人员给包起唯一名字的心智负担;
- 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性;
- 增加类型别名type alias支持大规模代码库的重构
- 多数功能不需要依赖外部的第三方包或库
- Go 开发者可以直接基于标准库提供的这些包实现一个满足生产要求的 API 服务,从而减少对外部第三方包或库的依赖,降低工程代码依赖管理的复杂性
- Go 语言就提供了足以让所有其它主流语言开发人员羡慕的工具链,工具链涵盖了编译构建、代码格式化、包依赖管理、静态代码检查、测试、文档生成与查看、性能剖析、语言服务器、运行时程序跟踪等方方面面。
- gofmt它统一了 Go 语言的代码风格在其他语言开发者还在为代码风格争论不休的时候Go 开发者可以更加专注于领域业务中。
- 在提供丰富的工具链的同时Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。
## 2. 环境
### 2.1 配好环境选择一种最适合你的Go安装方法
- 选择 Go 版本
- Go 语言的版本发布策略
- Go 团队已经将版本发布节奏稳定在每年发布两次大版本上,一般是在二月份和八月份发布
- 一般情况下,我建议你采用最新版本。因为 Go 团队发布的 Go 语言稳定版本的平均质量一直是很高的,少有影响使用的重大 bug。
- 使用次新版,即最新版本之前的那个版本比如,当前最新版本为 Go 1.17,那么这些项目会使用 Go 1.16 版本的最新补丁版本Go 1.16.x直到发布 Go 1.18 后,这些项目才会切换到 Go 1.17 的最新补丁版本Go 1.17.x
- 安装 Go
- **在 Linux 上安装 Go**
- 需要下载并解压 Go Linux 安装包:
- $wget -c https://golang.google.cn/dl/go1.16.5.linux-amd64.tar.gz
- 虽然 Go 官方下载站点是 golang.org/dl但我们可以用针对中国大陆的镜像站点 golang.google.cn/dl 来下载
- 第二步,将下载完毕的 Go 安装包解压到安装目录中:
- $tar -C /usr/local -xzf go1.16.5.linux-amd64.tar.gz
- 执行完上面解压缩命令后,我们将在 /usr/local 下面看到名为 go 的目录这个目录就是Go 的安装目录,也是 Go 官方推荐的 Go 安装目录。我们执行下面命令可以查看该安装目
录下的组成:
- > $ls -F /usr/local/go </br>
AUTHORS CONTRIBUTORS PATENTS SECURITY.md api/ doc/ lib/ </br>
CONTRIBUTING.md LICENSE README.md VERSION bin/ favicon.ico misc </br>
- 将 Go 二进制文件所在路径加入到用户环境变量 PATH 中,具体操作是将下面这行环境变量设置语句添加到 $HOME/.profile 文件的末尾:
- export PATH=$PATH:/usr/local/go/bin
- 然后执行下面命令使上述环境变量的设置立即生效:
- $source ~/.profile
- 最后,我们可以通过下面命令验证此次安装是否成功:
- $go version
- 如果这个命令输出了“go version go1.16.5 linux/amd64”那么说明我们这次的 Go 安装是成功的。
- **在 Mac 上安装 Go**
- 在 Mac 上我们可以在图形界面的引导下进行 Go 的安装。不过我们先要下载适用于Mac 的 Go 安装包:
- $wget -c https://golang.google.cn/dl/go1.16.5.darwin-amd64.pkg
- 安装包下载完毕后,我们可以双击安装包,和 Linux 一样Mac 上的 Go 安装包默认也会将 Go 安装到 /usr/local/go 路径下面,将这个路径加入到用户的环境变量 PATH中。具体操作方法与上面 Linux 中的步骤一样,也是将下面环境变量设置语句加入到$HOME/.profile 中,然后执行 source 命令让它生效就可以了:
- PS: 如果你安装了zsh等就要换一下环境变量使其生效
- export PATH=$PATH:/usr/local/go/bin
- 通过 go version 命令验证一下这次安装是否成功。
- **在 Windows 上安装 Go**
- 在页面上找到 go 1.16.5 版本的 Windows msi 安装包, 安装程序默认会把 Go 安装在 C:\ProgramFiles\Go 下面
- Go 安装程序还会自动为你设置好 Go 使用所需的环境变量,包括在用户环境变量中增加 GOPATH它的值默认为 C:\Users[用户名]\go在系统变量中也会为 Path 变量增加一个值C:\Program Files\Go\bin这样我们就可以在任意路径下使用 Go 了。
- 安装多个 Go 版本
- 首先,你需要按照标准步骤将 go 其他版本 安装到事先建好的 /usr/local/go.其他版本 路径 下:
- **方法一:重新设置 PATH 环境变量**
```shell
$mkdir /usr/local/go1.15.13
$wget -c https://golang.google.cn/dl/go1.15.13.linux-amd64.tar.gz
$tar -C /usr/local/go1.15.13 -xzf go1.15.13.linux-amd64.tar.gz
```
- 接下来,我们来设置 PATH 环境变量,将原先 $HOME/.profile 中的 PATH 变量的值由:
```shell
export PATH=$PATH:/usr/local/go/bin
```
- 改为
```shell
export PATH=$PATH:/usr/local/go1.15.13/go/bin
```
- 这样通过执行 source 命令重新使 PATH 环境变量生效后,我们再执行 go version 命令
```shell
$go version
go version go1.15.13 linux/amd64
```
- Go 官方也提供了一种在系统中安装多个 Go 版本的方法,下面我们就来看一下第二种方法。
- **方法二go get 命令**
- 这种方法有一个前提,那就是当前系统中已经通过标准方法安装过某个版本的 Go 了。
- HOME/go/bin加入到P ATH环境变量中并生效,即便HOME/go/bin 这个目录当前不存在也没关系:
```shell
export PATH=$PATH:/usr/local/go/bin:~/go/bin
```
- 然后,我们要执行下面这个命令安装 Go 1.15.13 版本的下载器:
```shell
$go get golang.org/dl/go1.15.13
```
- 这个命令会将名为 go1.15.13 的可执行文件安装到 $HOME/go/bin 这个目录下它是Go 1.15.13 版本的专用下载器,下面我们再来执行 Go 1.15.13 的下载安装命令:
```shell
$go1.15.13 download
Downloaded 0.0% ( 16384 / 121120420 bytes) ...
Downloaded 1.8% ( 2129904 / 121120420 bytes) ...
Downloaded 84.9% (102792432 / 121120420 bytes) ...
Downloaded 100.0% (121120420 / 121120420 bytes)
Unpacking /root/sdk/go1.15.13/go1.15.13.linux-amd64.tar.gz ...
Success. You may now run 'go1.15.13'
```
- 现在,我们看到这个命令下载了 go1.15.13.linux-amd64.tar.gz 安装包,也将它安装到 $HOME/sdk/go1.15.13 下面了。下载安装结束后,我们就可以利用带有版本号的 go 命
令来使用特定版本的 Go 了:
```shell
$go1.15.13 version
go version go1.15.13 linux/amd64
```
- 同样的,我们也可以通过下面这个命令查看特定 Go 版本的安装位置:
```shell
$go1.15.13 env GOROOT
/root/sdk/go1.15.13
```
- 配置 Go
- Go 的配置项是以环境变量的形式存在的,我们可以通过下面这个命令查看 Go 的这些配置项:
```shell
$go env
```
- 常用配置项:
- ![常用配置项](pic/常用配置项.png)
- 了解更多关于 Go 配置项的说明,你可以通过 go help environment 命令查看。
### 2.2 初窥门径一个Go程序的结构是怎样的
- 编写并运行第一个 Go 程序
- 首先,我们需要创建一个名为 main.go 的源文件。
- Go 的命名规则。Go 源文件总是用全小写字母形式的短小单词命名,并且以.go 扩展名结尾。
- 如果要在源文件的名字中使用多个单词,我们通常直接是将多个单词连接起来作为源文件名,而不是使用其他分隔符,比如下划线。也就是说,我们通常使用 helloworld.go 作为文件名而不是 hello_world.go。
- 这是因为下划线这种分隔符,在 Go 源文件命名中有特殊作用
- 尽量不要用两个以上的单词组合作为文件名,否则就很难分辨了
- main.go
```go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
```
- 编译和运行这个文件 - Linux
```shell
$go build main.go
$./main
hello, world
```
- 编译和运行这个文件 - Windows
```shell
>go build main.go
>.\main.exe
hello, world
```
- “helloworld”示例程序的结构
- package 定义了 Go 中的一个包 package。包是 Go 语言的基本组成单元,通常使用单个的小写单词命名,一个 Go 程序本质上就是一组包的集合。所有 Go 代码都有自己隶属的包
- main 包在 Go 中是一个特殊的包,整个 Go 程序中**仅允许存在一个名为 main 的包**。
- main 函数会比较特殊:当你运行一个可执行的 Go 程序的时候,所有的代码都会从这个入口函数开始运行。
- Gofmt 是 Go 语言在解决规模化scale问题上的一个最佳实践并成为了 Go 语言吸引其他语言开发者的一大卖点。作为 Go 开发人员,请在提交你的代码前使用 **Gofmt 格式化你的 Go 源码**
- 注意点
- 注意点 1标准 Go 代码风格使用 Tab 而不是空格来实现缩进的,当然这个代码风格的格式化工作也可以交由 gofmt 完成。
- 注意点 2我们调用了一个名为 Println 的函数,这个函数位于 Go 标准库的 fmt 包中。为了在我们的示例程序中使用 fmt 包定义的 Println 函数,我们其实做了两步操作。
- 注意点 3我们传入的字符串也就是我们执行程序后在终端的标准输出上看到的字符串。这种“所见即所得”得益于 Go 源码文件本身采用的是 Unicode 字符集而且用的是UTF-8 标准的字符编码方式
- 其实 Go 语言的正式语法规范是使用分号“;”来做结尾标识符的, gofmt 在按约定格式化代码时,会自动删除这些被我们手工加入的分号的
- Go 语言中程序是怎么编译的?
- Go 是一种编译型语言这意味着只有你编译完Go 程序之后,才可以将生成的可执行文件交付于其他人,并运行在没有安装 Go 的环境中。
- Go 提供了 run 命令可以直接运行 Go 源码文件
- 当然像 go run 这类命令更多用于开发调试阶段,真正的交付成果还是需要使用 go build
- 复杂项目下 Go 程序的编译是怎样的
- Go module 构建模式是在 Go 1.11 版本正式引入的,为的是彻底解决 Go 项目复杂版本依赖的问题,在 Go 1.16 版本中Go module 已经成为了 Go 默认的包依赖管理机制和Go 源码构建机制。
- Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。
- 其实,一个 module 就是一个包的集合,这些包和 module 一起打版本、发布和分发。go.mod 所在的目录被我们称为它声明的 module 的根目录
- 我们也可以使用 go mod tidy 命令,让 Go 工具自动添加第三方包
- go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。
### 2.3 标准先行Go项目的布局标准是什么
- Go 语言“创世项目”结构是怎样的?
- 用loccount 工具对 Go 语言发布的第一个Go 1.0 版本分析看看:
- ![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、app2cmd 目录下的各 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 下面有三个 modulemainmodule、
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")
}
```
- 你可以看到,这段代码依赖了第三方包 logruslogrus 是 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 代码块与作用域:如何保证变量不会被遮蔽?
### 3.3
## 并发
## 实战