1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-06-01 23:04:18 +00:00

Merge pull request #569 from iGmainC/master

全文优化排版
This commit is contained in:
chai2010 2022-02-05 06:08:04 +08:00 committed by GitHub
commit 19a4429be5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1677 additions and 1670 deletions

17
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: 'Gitbook Action Build'
on:
push:
branches:
- master # trigger branch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout action
uses: actions/checkout@v2
- name: Gitbook Action # https://github.com/ZanderZhao/gitbook-action/releases
uses: ZanderZhao/gitbook-action@v1.2.4 # -> or ZanderZhao/gitbook-action@master. If not use master click above, use latest please
with: # or fork this repo and use YourName/gitbook-action@master
token: ${{ secrets.PERSONAL_TOKEN }} # -> remember add this in settings/secrets as following
publish_branch: gh-pages
time_zone: Asia/Shanghai

View File

@ -1,6 +1,6 @@
# 1.6 常见的并发模式
Go语言最吸引人的地方是它内建的并发支持。Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSPCommunicating Sequential Process通讯顺序进程。CSP有着精确的数学模型并实际应用在了Hoare参与设计的T9000通用计算机上。从NewSqueak、Alef、Limbo到现在的Go语言对于对CSP有着20多年实战经验的Rob Pike来说他更关注的是将CSP应用在通用编程语言上产生的潜力。作为Go并发编程核心的CSP理论的核心概念只有一个同步通信。关于同步通信的话题我们在前面一节已经讲过本节我们将简单介绍下Go语言中常见的并发模式。
Go 语言最吸引人的地方是它内建的并发支持。Go 语言并发体系的理论是 _C.A.R Hoare_ 1978 年提出的 CSPCommunicating Sequential Process通讯顺序进程。CSP 有着精确的数学模型,并实际应用在了 Hoare 参与设计的 T9000 通用计算机上。从 NewSqueak、Alef、Limbo 到现在的 Go 语言,对于对 CSP 有着 20 多年实战经验的 _Rob Pike_ 来说,他更关注的是将 CSP 应用在通用编程语言上产生的潜力。作为 Go 并发编程核心的 CSP 理论的核心概念只有一个:同步通信。关于同步通信的话题我们在前面一节已经讲过,本节我们将简单介绍下 Go 语言中常见的并发模式。
首先要明确一个概念:并发不是并行。并发更关注的是程序的设计层面,并发的程序完全是可以顺序执行的,只有在真正的多核 CPU 上才可能真正地同时运行。并行更关注的是程序的运行层面,并行一般是简单的大量重复,例如 GPU 中对图像处理都会有大量的并行运算。为更好的编写并发程序,从设计之初 Go 语言就注重如何在编程语言层级上设计一个简洁安全高效的抽象模型,让程序员专注于分解问题和组合方案,而且不用被线程管理和信号互斥这些繁琐的操作分散精力。
@ -356,7 +356,6 @@ func main() {
}
```
不过 `gatefs` 对此做一个抽象类型 `gate`,增加了 `enter``leave` 方法分别对应并发代码的进入和离开。当超出并发数目限制的时候,`enter` 方法会阻塞直到并发数降下来为止。
```go
@ -368,7 +367,6 @@ func (g gate) leave() { <-g }
`gatefs` 包装的新的虚拟文件系统就是将需要控制并发的方法增加了 `enter``leave` 调用而已:
```go
type gatefs struct {
fs vfs.FileSystem
@ -384,7 +382,6 @@ func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
我们不仅可以控制最大的并发数目,而且可以通过带缓存 Channel 的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态,这对于后台一些低级任务的运行是有参考价值的。
## 1.6.5 赢者为王
采用并发编程的动机有很多:并发编程可以简化问题,比如一类问题对应一个处理线程会更简单;并发编程还可以提升性能,在一个多核 CPU 上开 2 个线程一般会比开 1 个线程快一些。其实对于提升性能而言,程序并不是简单地运行速度快就表示用户体验好的;很多时候程序能快速响应用户请求才是最重要的,当没有用户请求需要处理的时候才合适处理一些低优先级的后台任务。
@ -413,15 +410,13 @@ func main() {
通过适当开启一些冗余的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序的相应性能。
## 1.6.6 素数筛
在“Hello world 的革命”一节中,我们为了演示 Newsqueak 的并发特性,文中给出了并发版本素数筛的实现。并发版本的素数筛是一个经典的并发例子,通过它我们可以更深刻地理解 Go 语言的并发特性。“素数筛”的原理如图:
![](../images/ch1-13-prime-sieve.png)
*图 1-13 素数筛*
_图 1-13 素数筛_
我们需要先生成最初的 `2, 3, 4, ...` 自然数序列(不包含开头的 0、1
@ -478,7 +473,7 @@ func main() {
## 1.6.7 并发的安全退出
有时候我们需要通知goroutine停止它正在干的事情特别是当它工作在错误的方向上的时候。Go语言并没有提供在一个直接终止Goroutine的方法由于这样会导致goroutine之间的共享变量处在未定义的状态上。但是如果我们想要退出两个或者任意多个Goroutine怎么办呢
有时候我们需要通知 Goroutine 停止它正在干的事情特别是当它工作在错误的方向上的时候。Go 语言并没有提供在一个直接终止 Goroutine 的方法,由于这样会导致 Goroutine 之间的共享变量处在未定义的状态上。但是如果我们想要退出两个或者任意多个 Goroutine 怎么办呢?
Go 语言中不同 Goroutine 之间主要依靠管道进行通信和同步。要同时处理多个管道的发送或接收操作,我们需要使用 `select` 关键字(这个关键字和网络编程中的 `select` 函数的行为类似)。当 `select` 有多个分支时,会随机选择一个可用的管道分支,如果没有可用的管道分支则选择 `default` 分支,否则会一直保存阻塞状态。
@ -617,7 +612,6 @@ func main() {
现在每个工作者并发体的创建、运行、暂停和退出都是在 `main` 函数的安全控制之下了。
## 1.6.8 context 包
在 Go1.7 发布时,标准库增加了一个 `context` 包,用来简化对于处理单个请求的多个 Goroutine 之间与请求域的数据、超时和退出等操作,官方有博文对此做了专门介绍。我们可以用 `context` 包来重新实现前面的线程安全退出或超时的控制:
@ -771,7 +765,7 @@ func main() {
```
执行上面这个例子很容易就复现了死锁的问题,原因是素数筛中的 `ctx.Done()` 位于 `if i := <-in; i%prime != 0` 判断之内,
而这个判断可能会一直阻塞,导致goroutine无法正常退出。让我们来解决这个问题。
而这个判断可能会一直阻塞,导致 Goroutine 无法正常退出。让我们来解决这个问题。
```go
package main
@ -907,6 +901,7 @@ func main() {
```
在上面这个例子中主要有以下几点需要关注:
1. 通过 `for range` 循环保证了输入管道被关闭时,循环能退出,不会出现死循环;
2. 通过 `defer close` 保证了无论是输入管道被关闭,还是 ctx 被取消,只要素数筛退出,都会关闭输出管道。

View File

@ -379,7 +379,7 @@ func main() {
// 虽然总是返回 nil, 但是可以恢复异常状态
}()
// 警告: 用`nil`为参数抛出异常
// 警告: 用 nil 为参数抛出异常
panic(nil)
}
```

View File

@ -8,7 +8,7 @@ Go语言中很多设计思想和工具都是传承自Plan9操作系统Go汇
无论高级语言如何发展,作为最接近 CPU 的汇编语言的地位依然是无法彻底被替代的。只有通过汇编语言才能彻底挖掘 CPU 芯片的全部功能,因此操作系统的引导过程必须要依赖汇编语言的帮助。只有通过汇编语言才能彻底榨干 CPU 芯片的性能,因此很多底层的加密解密等对性能敏感的算法会考虑通过汇编语言进行性能优化。
对于每一个严肃的GopherGo汇编语言都是一个不可忽视的技术。因为哪怕只懂一点点汇编也便于更好地理解计算机原理也更容易理解Go语言中动态栈/接口等高级特性的实现原理。而且掌握了Go汇编语言之后你将重新站在编程语言鄙视链的顶端不用担心再被任何其它所谓的高级编程语言用户鄙视。
对于每一个严肃的 GopherGo 汇编语言都是一个不可忽视的技术。因为哪怕只懂一点点汇编,也便于更好地理解计算机原理,也更容易理解 Go 语言中动态栈、接口等高级特性的实现原理。而且掌握了 Go 汇编语言之后,你将重新站在编程语言鄙视链的顶端,不用担心再被任何其它所谓的高级编程语言用户鄙视。
本章我们将以 AMD64 为主要开发环境,简单地探讨 Go 汇编语言的基础用法。

View File

@ -2,7 +2,7 @@
RPC 是远程过程调用的简称是分布式系统中不同节点间流行的通信方式。在互联网时代RPC 已经和 IPC 一样成为一个不可或缺的基础构件。因此 Go 语言的标准库也提供了一个简单的 RPC 实现,我们将以此为入口学习 RPC 的各种用法。
## 4.1.1 RPC版"Hello, World"
## 4.1.1 RPC 版 “Hello, World”
Go 语言的 RPC 包的路径为 net/rpc也就是放在了 net 包目录下面。因此我们可以猜测该 RPC 包是建立在 net 包基础之上的。在第一章 “Hello, World” 革命一节最后,我们基于 http 实现了一个打印例子。下面我们尝试基于 rpc 实现一个类似的例子。

View File

@ -77,7 +77,7 @@ type Plugin interface {
}
```
我们需要在Generate和GenerateImports函数中分别生成相关的代码。而Protobuf文件的全部信息都在*generator.FileDescriptor类型函数参数中描述因此我们需要从函数参数中提前扩展定义的元数据。
我们需要在 Generate GenerateImports 函数中分别生成相关的代码。而 Protobuf 文件的全部信息都在 `*generator.FileDescriptor` 类型函数参数中描述,因此我们需要从函数参数中提前扩展定义的元数据。
pbgo 框架中的插件对象是 pbgoPlugin在 Generate 方法中首先需要遍历 Protobuf 文件中定义的全部服务,然后再遍历每个服务的每个方法。在得到方法结构之后再通过自定义的 getServiceMethodOption 方法提取 rest 扩展信息:

View File

@ -1,5 +1,3 @@
## 4.9 补充说明
目前专门讲述 RPC 的图书比较少。目前 Protobuf 和 gRPC 的官网都提供了详细的参考资料和例子。本章重点讲述了 Go 标准库的 RPC 和基于 Protobuf 衍生的 gRPC 框架,同时也简单展示了如何自己定制一个 RPC 框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是 Go 语言团队官方在进行维护,和 Go 语言契合也最为默契。不过 RPC 依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的 RPC 框架,还有针对分布式系统进行深度定制的 RPC 系统,用户可以根据自己实际需求选择合适的工具。

View File

@ -7,10 +7,8 @@
如果我们不考虑均衡的话,现在有 n 个服务节点,我们完成业务流程只需要从这 n 个中挑出其中的一个。有几种思路:
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把服务节点信息都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
2. 随机挑一个: 每次都随机挑,真随机伪随机均可。假设选择第 x 台机器,那么 x 可描述为 `rand.Intn()%n`
3. 根据某种权重,对下游节点进行排序,选择权重最大/小的那一个。
3. 根据某种权重,对下游节点进行排序,选择权重最大或最小的那一个。
当然了,实际场景我们不可能无脑轮询或者无脑随机,如果对下游请求失败了,我们还需要某种机制来进行重试,如果纯粹的随机算法,存在一定的可能性使你在下一次仍然随机到这次的问题节点。
@ -72,7 +70,6 @@ func request(params map[string]interface{}) error {
真的没有问题么?还是有问题的。这段简短的程序里有两个隐藏的隐患:
1. 没有随机种子。在没有随机种子的情况下,`rand.Intn()` 返回的伪随机数序列是固定的。
2. 洗牌不均匀,会导致整个数组第一个节点有大概率被选中,并且多个节点的负载分布不均衡。
第一点比较简单,应该不用在这里给出证明了。关于第二点,我们可以用概率知识来简单证明一下。假设每次挑选都是真随机,我们假设第一个位置的节点在 `len(slice)` 次交换中都不被选中的概率是 `((6/7)*(6/7))^7≈0.34`。而分布均匀的情况下,我们肯定希望被第一个元素在任意位置上分布的概率均等,所以其被随机选到的概率应该约等于 `1/7≈0.14`