diff --git a/SUMMARY.md b/SUMMARY.md index 7ca581c..d02c199 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,69 +1,69 @@ # 目录 -* [第一章 语言基础](ch1-basic/readme.md) - * [1.1. Go语言创世纪](ch1-basic/ch1-01-genesis.md) - * [1.2. Hello, World 的革命](ch1-basic/ch1-02-hello-revolution.md) - * [1.3. 数组、字符串和切片](ch1-basic/ch1-03-array-string-and-slice.md) - * [1.4. 函数、方法和接口](ch1-basic/ch1-04-func-method-interface.md) - * [1.5. 面向并发的内存模型](ch1-basic/ch1-05-mem.md) - * [1.6. 常见的并发模式](ch1-basic/ch1-06-goroutine.md) - * [1.7. 错误和异常](ch1-basic/ch1-07-error-and-panic.md) - * [1.8. 补充说明](ch1-basic/ch1-08-ext.md) -* [第二章 CGO编程](ch2-cgo/readme.md) - * [2.1. 快速入门](ch2-cgo/ch2-01-hello-cgo.md) - * [2.2. CGO基础](ch2-cgo/ch2-02-basic.md) - * [2.3. 类型转换](ch2-cgo/ch2-03-cgo-types.md) - * [2.4. 函数调用](ch2-cgo/ch2-04-func.md) - * [2.5. 内部机制](ch2-cgo/ch2-05-internal.md) - * [2.6. 实战: 封装qsort](ch2-cgo/ch2-06-qsort.md) - * [2.7. CGO内存模型](ch2-cgo/ch2-07-memory.md) - * [2.8. C++类包装](ch2-cgo/ch2-08-class.md) - * [2.9. 静态库和动态库](ch2-cgo/ch2-09-static-shared-lib.md) - * [2.10. 编译和链接参数](ch2-cgo/ch2-10-link.md) - * [2.11. 补充说明](ch2-cgo/ch2-11-ext.md) -* [第三章 汇编语言](ch3-asm/readme.md) - * [3.1. 快速入门](ch3-asm/ch3-01-basic.md) - * [3.2. 计算机结构](ch3-asm/ch3-02-arch.md) - * [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md) - * [3.4. 函数](ch3-asm/ch3-04-func.md) - * [3.5. 控制流](ch3-asm/ch3-05-control-flow.md) - * [3.6. 再论函数](ch3-asm/ch3-06-func-again.md) - * [3.7. 汇编语言的威力](ch3-asm/ch3-07-hack-asm.md) - * [3.8. 例子:Goroutine ID](ch3-asm/ch3-08-goroutine-id.md) - * [3.9. Delve调试器](ch3-asm/ch3-09-debug.md) - * [3.10. 补充说明](ch3-asm/ch3-10-ext.md) -* [第四章 RPC和Protobuf](ch4-rpc/readme.md) - * [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md) - * [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md) - * [4.3. 玩转RPC](ch4-rpc/ch4-03-netrpc-hack.md) - * [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md) - * [4.5. GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md) - * [4.6. GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md) - * [4.7. pbgo: 基于Protobuf的框架](ch4-rpc/ch4-07-pbgo.md) - * [4.8. 补充说明](ch4-rpc/ch4-08-ext.md) -* [第五章 Go和Web](ch5-web/readme.md) - * [5.1. Web开发简介](ch5-web/ch5-01-introduction.md) - * [5.2. Router请求路由](ch5-web/ch5-02-router.md) - * [5.3. Middleware中间件](ch5-web/ch5-03-middleware.md) - * [5.4. Validator请求校验](ch5-web/ch5-04-validator.md) - * [5.5. Database和数据库打交道](ch5-web/ch5-05-database.md) - * [5.6. Ratelimit 服务流量限制](ch5-web/ch5-06-ratelimit.md) - * [5.7. Layout大型web项目分层](ch5-web/ch5-07-layout-of-web-project.md) - * [5.8. interface 和 table-driven 开发](ch5-web/ch5-08-interface-and-web.md) - * [5.9. 灰度发布和 A/B test](ch5-web/ch5-09-gated-launch.md) - * [5.11. Load-balance负载均衡](ch5-web/ch5-11-load-balance.md) -* [第六章 分布式系统](ch6-cloud/readme.md) - * [6.1. 云上地鼠(TODO)](ch6-cloud/ch6-01-cloud.md) - * [6.2. 分布式搜索引擎](ch6-cloud/ch6-02-search-engine.md) - * [6.2. Raft协议(TODO)](ch6-cloud/ch6-03-raft.md) - * [6.4. 分布式队列(TODO)](ch6-cloud/ch6-04-queue.md) - * [6.5. 分布式缓存(TODO)](ch6-cloud/ch6-05-cache.md) - * [6.6. etcd(TODO)](ch6-cloud/ch6-06-etcd.md) - * [6.7. 分布式 id 生成器](ch6-cloud/ch6-07-dist-id.md) - * [6.8. 分布式锁(Doing)](ch6-cloud/ch6-08-lock.md) - * [6.9. 分布式任务调度系统(TODO)](ch6-cloud/ch6-09-sched.md) - * [6.10. 延时任务系统](ch6-cloud/ch6-10-delay-job.md) - * [6.12. 补充说明(TODO)](ch6-cloud/ch6-11-faq.md) +* [第1章 语言基础](ch1-basic/readme.md) + * [1.1 Go语言创世纪](ch1-basic/ch1-01-genesis.md) + * [1.2 Hello, World 的革命](ch1-basic/ch1-02-hello-revolution.md) + * [1.3 数组、字符串和切片](ch1-basic/ch1-03-array-string-and-slice.md) + * [1.4 函数、方法和接口](ch1-basic/ch1-04-func-method-interface.md) + * [1.5 面向并发的内存模型](ch1-basic/ch1-05-mem.md) + * [1.6 常见的并发模式](ch1-basic/ch1-06-goroutine.md) + * [1.7 错误和异常](ch1-basic/ch1-07-error-and-panic.md) + * [1.8 补充说明](ch1-basic/ch1-08-ext.md) +* [第2章 CGO编程](ch2-cgo/readme.md) + * [2.1 快速入门](ch2-cgo/ch2-01-hello-cgo.md) + * [2.2 CGO基础](ch2-cgo/ch2-02-basic.md) + * [2.3 类型转换](ch2-cgo/ch2-03-cgo-types.md) + * [2.4 函数调用](ch2-cgo/ch2-04-func.md) + * [2.5 内部机制](ch2-cgo/ch2-05-internal.md) + * [2.6 实战: 封装qsort](ch2-cgo/ch2-06-qsort.md) + * [2.7 CGO内存模型](ch2-cgo/ch2-07-memory.md) + * [2.8 C++类包装](ch2-cgo/ch2-08-class.md) + * [2.9 静态库和动态库](ch2-cgo/ch2-09-static-shared-lib.md) + * [2.10 编译和链接参数](ch2-cgo/ch2-10-link.md) + * [2.11 补充说明](ch2-cgo/ch2-11-ext.md) +* [第3章 汇编语言](ch3-asm/readme.md) + * [3.1 快速入门](ch3-asm/ch3-01-basic.md) + * [3.2 计算机结构](ch3-asm/ch3-02-arch.md) + * [3.3 常量和全局变量](ch3-asm/ch3-03-const-and-var.md) + * [3.4 函数](ch3-asm/ch3-04-func.md) + * [3.5 控制流](ch3-asm/ch3-05-control-flow.md) + * [3.6 再论函数](ch3-asm/ch3-06-func-again.md) + * [3.7 汇编语言的威力](ch3-asm/ch3-07-hack-asm.md) + * [3.8 例子:Goroutine ID](ch3-asm/ch3-08-goroutine-id.md) + * [3.9 Delve调试器](ch3-asm/ch3-09-debug.md) + * [3.10 补充说明](ch3-asm/ch3-10-ext.md) +* [第4章 RPC和Protobuf](ch4-rpc/readme.md) + * [4.1 RPC入门](ch4-rpc/ch4-01-rpc-intro.md) + * [4.2 Protobuf](ch4-rpc/ch4-02-pb-intro.md) + * [4.3 玩转RPC](ch4-rpc/ch4-03-netrpc-hack.md) + * [4.4 GRPC入门](ch4-rpc/ch4-04-grpc.md) + * [4.5 GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md) + * [4.6 GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md) + * [4.7 pbgo: 基于Protobuf的框架](ch4-rpc/ch4-07-pbgo.md) + * [4.8 补充说明](ch4-rpc/ch4-08-ext.md) +* [第5章 Go和Web](ch5-web/readme.md) + * [5.1 Web开发简介](ch5-web/ch5-01-introduction.md) + * [5.2 Router请求路由](ch5-web/ch5-02-router.md) + * [5.3 Middleware中间件](ch5-web/ch5-03-middleware.md) + * [5.4 Validator请求校验](ch5-web/ch5-04-validator.md) + * [5.5 Database和数据库打交道](ch5-web/ch5-05-database.md) + * [5.6 Ratelimit 服务流量限制](ch5-web/ch5-06-ratelimit.md) + * [5.7 Layout大型web项目分层](ch5-web/ch5-07-layout-of-web-project.md) + * [5.8 interface 和 table-driven 开发](ch5-web/ch5-08-interface-and-web.md) + * [5.9 灰度发布和 A/B test](ch5-web/ch5-09-gated-launch.md) + * [5.11 Load-balance负载均衡](ch5-web/ch5-11-load-balance.md) +* [第6章 分布式系统](ch6-cloud/readme.md) + * [6.1 云上地鼠(TODO)](ch6-cloud/ch6-01-cloud.md) + * [6.2 分布式搜索引擎](ch6-cloud/ch6-02-search-engine.md) + * [6.2 Raft协议(TODO)](ch6-cloud/ch6-03-raft.md) + * [6.4 分布式队列(TODO)](ch6-cloud/ch6-04-queue.md) + * [6.5 分布式缓存(TODO)](ch6-cloud/ch6-05-cache.md) + * [6.6 etcd(TODO)](ch6-cloud/ch6-06-etcd.md) + * [6.7 分布式 id 生成器](ch6-cloud/ch6-07-dist-id.md) + * [6.8 分布式锁(Doing)](ch6-cloud/ch6-08-lock.md) + * [6.9 分布式爬虫(TODO)](ch6-cloud/ch6-09-scrawler.md) + * [6.10 延时任务系统](ch6-cloud/ch6-10-delay-job.md) + * [6.11 补充说明(TODO)](ch6-cloud/ch6-11-faq.md) * [附录](appendix/readme.md) * [附录A: Go语言常见坑](appendix/appendix-a-trap.md) * [附录B: 有趣的代码片段](appendix/appendix-b-gems.md) diff --git a/ch1-basic/ch1-01-genesis.md b/ch1-basic/ch1-01-genesis.md index 5a0c2c3..de06cc3 100644 --- a/ch1-basic/ch1-01-genesis.md +++ b/ch1-basic/ch1-01-genesis.md @@ -1,4 +1,4 @@ -# 1.1. Go语言创世纪 +# 1.1 Go语言创世纪 Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三个大牛于2007年开始设计发明,设计新语言的最初的洪荒之力来自于对超级复杂的C++11特性的吹捧报告的鄙视,最终的目标是设计网络和多核时代的C语言。到2008年中期,语言的大部分特性设计已经完成,并开始着手实现编译器和运行时,大约在这一年Russ Cox作为主力开发者加入。到了2010年,Go语言已经逐步趋于稳定,并在9月正式发布Go语言并开源了代码。 diff --git a/ch1-basic/ch1-02-hello-revolution.md b/ch1-basic/ch1-02-hello-revolution.md index 6f7a6ca..bc31139 100644 --- a/ch1-basic/ch1-02-hello-revolution.md +++ b/ch1-basic/ch1-02-hello-revolution.md @@ -1,4 +1,4 @@ -# 1.2. Hello, World 的革命 +# 1.2 Hello, World 的革命 在创世纪章节中我们简单介绍了Go语言的演化基因族谱,对其中来自于贝尔实验室的特有并发编程基因做了重点介绍,最后引出了Go语言版的“Hello, World”程序。其实“Hello, World”程序是展示各种语言特性的最好的例子,是通向该语言的一个窗口。这一节我们将沿着各个编程语言演化的时间轴,简单回顾下“Hello, World”程序是如何逐步演化到目前的Go语言形式、最终完成它的革命使命的。 diff --git a/ch1-basic/ch1-03-array-string-and-slice.md b/ch1-basic/ch1-03-array-string-and-slice.md index ae914d6..58171af 100644 --- a/ch1-basic/ch1-03-array-string-and-slice.md +++ b/ch1-basic/ch1-03-array-string-and-slice.md @@ -1,4 +1,4 @@ -# 1.3. 数组、字符串和切片 +# 1.3 数组、字符串和切片 在主流的编程语言中数组及其相关的数据结构是使用得最为频繁的,只有在它(们)不能满足时才会考虑链表、hash表(hash表可以看作是数组和链表的混合体)和更复杂的自定义数据结构。 diff --git a/ch1-basic/ch1-04-func-method-interface.md b/ch1-basic/ch1-04-func-method-interface.md index 22ba086..48aacba 100644 --- a/ch1-basic/ch1-04-func-method-interface.md +++ b/ch1-basic/ch1-04-func-method-interface.md @@ -1,4 +1,4 @@ -# 1.4. 函数、方法和接口 +# 1.4 函数、方法和接口 函数对应操作序列,是程序的基本组成元素。Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。方法是绑定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定。接口定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定的。Go语言通过隐式接口机制实现了鸭子面向对象模型。 diff --git a/ch1-basic/ch1-05-mem.md b/ch1-basic/ch1-05-mem.md index 3aa59d5..a39b025 100644 --- a/ch1-basic/ch1-05-mem.md +++ b/ch1-basic/ch1-05-mem.md @@ -1,4 +1,4 @@ -# 1.5. 面向并发的内存模型 +# 1.5 面向并发的内存模型 在早期,CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行,在相同的时刻有且仅有一个CPU在顺序执行程序的指令。 diff --git a/ch1-basic/ch1-06-goroutine.md b/ch1-basic/ch1-06-goroutine.md index 7408256..4235731 100644 --- a/ch1-basic/ch1-06-goroutine.md +++ b/ch1-basic/ch1-06-goroutine.md @@ -1,4 +1,4 @@ -# 1.6. 常见的并发模式 +# 1.6 常见的并发模式 Go语言最吸引人的地方是它内建的并发支持。Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSP(Communicating Sequential Process,通讯顺序进程)。CSP有着精确的数学模型,并实际应用在了Hoare参与设计的T9000通用计算机上。从NewSqueak、Alef、Limbo到现在的Go语言,对于对CSP有着20多年实战经验的Rob Pike来说,他更关注的是将CSP应用在通用编程语言上的潜力。作为Go并发编程核心的CSP理论的核心概念只有一个:同步通信。关于同步通信的话题我们在前面一节已经讲过,本节我们将简单介绍下Go语言中常见的并发模式。 diff --git a/ch1-basic/ch1-07-error-and-panic.md b/ch1-basic/ch1-07-error-and-panic.md index 060ee3d..61a1385 100644 --- a/ch1-basic/ch1-07-error-and-panic.md +++ b/ch1-basic/ch1-07-error-and-panic.md @@ -1,429 +1,429 @@ -# 1.7. 错误和异常 - -错误处理是每个编程语言都要考虑的一个重要话题。在Go语言的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分。 - -在程序中总有一部分函数总是要求必须能够成功的运行。比如`strconv.Itoa`将整数转换为字符串,从数组或切片中读写元素,从`map`读取已经存在的元素等。这类操作在运行时几乎不会失败,除非程序中有BUG,或遇到灾难性的、不可预料的情况,比如运行时的内存溢出。如果真的遇到真正异常情况,我们只要简单终止程序就可以了。 - -排除异常的情况,如果程序运行失败仅被认为是几个预期的结果之一。对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,当从一个`map`查询一个结果时,可以通过额外的布尔值判断是否成功: - -```go -if v, ok := m["key"]; ok { - return v -} -``` - -但是导致失败的原因通常不止一种,很多时候用户希望了解更多的错误信息。如果只是用简单的布尔类型的状态值将不能满足这个要求。在C语言中,默认采用一个整数类型的`errno`来表达错误,这样就可以根据需要定义多种错误类型。在Go语言中,`syscall.Errno`就是对应C语言中`errno`类型的错误。在`syscall`包中的接口,如果有返回错误的话,底层也是`syscall.Errno`错误类型。 - -比如我们通过`syscall`包的接口来修改文件的模式时,如果遇到错误我们可以将`err`强制断言为`syscall.Errno`错误类型处理: - -```go -err := syscall.Chmod(":invalid path:", 0666) -if err != nil { - log.Fatal(err.(syscall.Errno)) -} -``` - -我们还可以进一步地通过类型查询或类型断言来获取底层真实的错误类型,这样就可以获取更详细的错误信息。不过一般情况下我们并不关心错误在底层的表达方式,我们只需要知道它是一个错误就可以了。当返回的错误值不是`nil`时,我们可以通过调用`error`接口类型的`Error`方法来获得字符串类型的错误信息。 - -在Go语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG或发生了其它不可控的问题。Go语言推荐使用`recover`函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。 - -如果某个接口简单地将所有普通的错误当做异常抛出,将会使错误信息杂乱且没有价值。就像在`main`函数中直接捕获全部一样,是没有意义的: - -```go -func main() { - defer func() { - if r := recover(); r != nil { - log.Fatal(r) - } - }() - - ... -} -``` - -捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式。 - -## 错误处理策略 - -让我们演示一个文件复制的例子:函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件: - -```go -func CopyFile(dstName, srcName string) (written int64, err error) { - src, err := os.Open(srcName) - if err != nil { - return - } - - dst, err := os.Create(dstName) - if err != nil { - return - } - - written, err = io.Copy(dst, src) - dst.Close() - src.Close() - return -} -``` - -上面的代码虽然能够工作,但是隐藏一个bug。如果第一个`os.Open`调用成功,但是第二个`os.Create`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭: - -```go -func CopyFile(dstName, srcName string) (written int64, err error) { - src, err := os.Open(srcName) - if err != nil { - return - } - defer src.Close() - - dst, err := os.Create(dstName) - if err != nil { - return - } - defer dst.Close() - - return io.Copy(dst, src) -} -``` - -`defer`语句可以让我们在打开文件时马上思考如何关闭文件。不管函数如何返回,文件关闭语句始终会被执行。同时`defer`语句可以保证,即使`io.Copy`发生了异常,文件依然可以安全地关闭。 - -前文我们说到,Go语言中的导出函数一般不抛出异常,一个未受控的异常可以看作是程序的BUG。但是对于那些提供类似Web服务的框架而言;它们经常需要接入第三方的中间件。因为第三方的中间件是否存在BUG是否会抛出异常,Web框架本身是不能确定的。为了提高系统的稳定性,Web框架一般会通过`recover`来防御性地捕获所有处理流程中可能产生的异常,然后将异常转为普通的错误返回。 - -让我们以JSON解析器为例,说明recover的使用场景。考虑到JSON解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。 - -```go -func ParseJSON(input string) (s *Syntax, err error) { - defer func() { - if p := recover(); p != nil { - err = fmt.Errorf("JSON: internal error: %v", p) - } - }() - // ...parser... -} -``` - -在标准库中的`json`包,在内部递归解析JSON数据的时候如果遇到错误,会通过抛出异常的方式来快速跳出深度嵌套的函数调用,然后由最外一级的接口通过`recover`捕获`panic`,然后返回相应的错误信息。 - -Go语言库的实现习惯: 即使在包内部使用了`panic`,但是在导出函数时会被转化为明确的错误值。 - -# 获取错误的上下文 - -有时候为了方便上层用户理解;很多时候底层实现者会将底层的错误重新包装为新的错误类型返回给用户: - -```go -if _, err := html.Parse(resp.Body); err != nil { - return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) -} -``` - -上层用户在遇到错误时,可以很容易从业务层面理解错误发生的原因。但是鱼和熊掌总是很难兼得,在上层用户获得新的错误的同时,我们也丢失了底层最原始的错误类型(只剩下错误描述信息了)。 - -为了记录这种错误类型在包装的变迁过程中的信息,我们一般会定义一个辅助的`WrapError`函数,用于包装原始的错误,同时保留完整的原始错误类型。为了问题定位的方便,同时也为了能记录错误发生时的函数调用状态,我们很多时候希望在出现致命错误的时候保存完整的函数调用信息。同时,为了支持RPC等跨网络的传输,我们可能要需要将错误序列化为类似JSON格式的数据,然后再从这些数据中将错误解码恢复出来。 - -为此,我们可以定义自己的`github.com/chai2010/errors`包,里面是以下的错误类型: - -```go - -type Error interface { - Caller() []CallerInfo - Wraped() []error - Code() int - error - - private() -} - -type CallerInfo struct { - FuncName string - FileName string - FileLine int -} -``` - -其中`Error`为接口类型,是`error`接口类型的扩展,用于给错误增加调用栈信息,同时支持错误的多级嵌套包装,支持错误码格式。为了使用方便,我们可以定义以下的辅助函数: - -```go -func New(msg string) error -func NewWithCode(code int, msg string) error - -func Wrap(err error, msg string) error -func WrapWithCode(code int, err error, msg string) error - -func FromJson(json string) (Error, error) -func ToJson(err error) string -``` - -`New`用于构建新的错误类型,和标准库中`errors.New`功能类似,但是增加了出错误时的函数调用栈信息。`FromJson`用于从JSON字符串编码的错误中恢复错误对象。`NewWithCode`则是构造一个带错误码的错误,同时也包含出错误时的函数调用栈信息。`Wrap`和`WrapWithCode`则是错误二次包装函数,用于将底层的错误包装为新的错误,但是保留的原始的底层错误信息。这里返回的错误对象都可以直接调用`json.Marshal`将错误编码为JSON字符串。 - -我们可以这样使用包装函数: - -```go -import ( - "github.com/chai2010/errors" -) - -func loadConfig() error { - _, err := ioutil.ReadFile("/path/to/file") - if err != nil { - return errors.Wrap(err, "read failed") - } - - // ... -} - -func setup() error { - err := loadConfig() - if err != nil { - return errors.Wrap(err, "invalid config") - } - - // ... -} - -func main() { - if err := setup(); err != nil { - log.Fatal(err) - } - - // ... -} -``` - -上面的例子中,错误被进行了2层包装。我们可以这样遍历原始错误经历了哪些包装流程: - -```go - for i, e := range err.(errors.Error).Wraped() { - fmt.Printf("wraped(%d): %v\n", i, e) - } -``` - -同时也可以获取每个包装错误的函数调用堆栈信息: - -```go - for i, x := range err.(errors.Error).Caller() { - fmt.Printf("caller:%d: %s\n", i, x.FuncName) - } -``` - -如果需要将错误通过网络传输,可以用`errors.ToJson(err)`编码为JSON字符串: - -```go -// 以JSON字符串方式发送错误 -func sendError(ch chan<- string, err error) { - ch <- errors.ToJson(err) -} - -// 接收JSON字符串格式的错误 -func recvError(ch <-chan string) error { - p, err := errors.FromJson(<-ch) - if err != nil { - log.Fatal(err) - } - return p -} -``` - -对于基于http协议的网络服务,我们还可以给错误绑定一个对应的http状态码: - -```go -err := errors.NewWithCode(404, "http error code") - -fmt.Println(err) -fmt.Println(err.(errors.Error).Code()) -``` - -在Go语言中,错误处理也有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在`else`语句块中,而应直接放在函数体中。 - -```go -f, err := os.Open("filename.ext") -if err != nil { - // 失败的情形, 马上返回错误 -} - -// 正常的处理流程 -``` - -Go语言中大部分函数的代码结构几乎相同,首先是一系列的初始检查,用于防止错误发生,之后是函数的实际逻辑。 - - -# 错误的错误返回 - -Go语言中的错误是一种接口类型。接口信息中包含了原始类型和原始的值。只有当接口的类型和原始的值都为空的时候,接口的值才对应`nil`。其实当接口中类型为空的时候,原始值必然也是空的;反之,当接口对应的原始值为空的时候,接口对应的原始类型并不一定为空的。 - -在下面的例子中,试图返回自定义的错误类型,当没有错误的时候返回`nil`: - -```go -func returnsError() error { - var p *MyError = nil - if bad() { - p = ErrBad - } - return p // Will always return a non-nil error. -} -``` - -但是,最终返回的结果其实并非是`nil`:是一个正常的错误,错误的值是一个`MyError`类型的空指针。下面是改进的`returnsError`: - -```go -func returnsError() error { - if bad() { - return (*MyError)(err) - } - return nil -} -``` - -因此,在处理错误返回值的时候,没有错误的返回值最好直接写为`nil`。 - -Go语言作为一个强类型语言,不同类型之间必须要显式的转换(而且必须有相同的基础类型)。但是,Go语言中`interface`是一个例外:非接口类型到接口类型,或者是接口类型之间的转换都是隐式的。这是为了支持方便的鸭子面向对象编程,当然会牺牲一定的安全特性。 - -# 剖析异常 - -`panic`支持抛出任意类型的异常(而不仅仅是`error`类型的错误),`recover`函数调用的返回值和`panic`函数的输入参数类型一致,它们的函数签名如下: - -```go -func panic(interface{}) -func recover() interface{} -``` - -Go语言函数调用的正常流程是函数执行返回语句返回结果,在这个流程中是没有异常的,因此在这个流程中执行`recover`异常捕获函数始终是返回`nil`。另一种是异常流程: 当函数调用`panic`抛出异常,函数将停止执行后续的普通语句,但是之前注册的`defer`函数调用仍然保证会被正常执行,然后再返回到的调用者。对于当前函数的调用者,因为处理异常状态还没有被捕获,和直接调用`panic`函数的行为类似。在异常发生时,如果在`defer`中执行`recover`调用,它可以捕获触发`panic`时的参数,并且恢复到正常的执行流程。 - -在非`defer`语句中执行`recover`调用是初学者常犯的错误: - -```go -func main() { - if r := recover(); r != nil { - log.Fatal(r) - } - - panic(123) - - if r := recover(); r != nil { - log.Fatal(r) - } -} -``` - -上面程序中两个`recover`调用都不能捕获任何异常。在第一个`recover`调用执行时,函数必然是在正常的非异常执行流程中,这时候`recover`调用将返回`nil`。发生异常时,第二个`recover`调用将没有机会被执行到,因为`panic`调用会导致函数马上执行已经注册`defer`的函数后返回。 - -其实`recover`函数调用有着更严格的要求:我们必须在`defer`函数中直接调用`recover`。如果`defer`中调用的是`recover`函数的包装函数的话,异常的捕获工作将失败!比如,有时候我们可能希望包装自己的`MyRecover`函数,在内部增加必要的日志信息然后再调用`recover`,这是错误的做法: - -```go -func main() { - defer func() { - // 无法捕获异常 - if r := MyRecover(); r != nil { - fmt.Println(r) - } - }() - panic(1) -} - -func MyRecover() interface{} { - log.Println("trace...") - return recover() -} -``` - -同样,如果是在嵌套的`defer`函数中调用`recover`也将导致无法捕获异常: - -```go -func main() { - defer func() { - defer func() { - // 无法捕获异常 - if r := recover(); r != nil { - fmt.Println(r) - } - }() - }() - panic(1) -} -``` - -2层嵌套的`defer`函数中直接调用`recover`和1层`defer`函数中调用包装的`MyRecover`函数一样,都是经过了2个函数帧才到达真正的`recover`函数,这个时候Goroutine的对应上一级栈帧中已经没有异常信息。 - -如果我们直接在`defer`语句中调用`MyRecover`函数又可以正常工作了: - -```go -func MyRecover() interface{} { - return recover() -} - -func main() { - // 可以正常捕获异常 - defer MyRecover() - panic(1) -} -``` - -但是,如果`defer`语句直接调用`recover`函数,依然不能正常捕获异常: - -```go -func main() { - // 无法捕获异常 - defer recover() - panic(1) -} -``` - -必须要和有异常的栈帧只隔一个栈帧,`recover`函数才能正常捕获异常。换言之,`recover`函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层`defer`函数)! - -当然,为了避免`recover`调用者不能识别捕获到的异常, 应该避免用`nil`为参数抛出异常: - -```go -func main() { - defer func() { - if r := recover(); r != nil { ... } - // 虽然总是返回nil, 但是可以恢复异常状态 - }() - - // 警告: 用`nil`为参数抛出异常 - panic(nil) -} -``` - -当希望将捕获到的异常转为错误时,如果希望忠实返回原始的信息,需要针对不同的类型分别处理: - -```go -func foo() (err error) { - defer func() { - if r := recover(); r != nil { - switch x := r.(type) { - case string: - err = errors.New(x) - case error: - err = x - default: - err = fmt.Errorf("Unknown panic: %v", r) - } - } - }() - - panic("TODO") -} -``` - -基于这个代码模板,我们甚至可以模拟出不同类型的异常。通过为定义不同类型的保护接口,我们就可以区分异常的类型了: - -```go -func main { - defer func() { - if r := recover(); r != nil { - switch x := r.(type) { - case runtime.Error: - // 这是运行时错误类型异常 - case error: - // 普通错误类型异常 - default: - // 其他类型异常 - } - } - }() - - ... -} -``` - -不过这样做和Go语言简单直接的编程哲学背道而驰了。 +# 1.7 错误和异常 + +错误处理是每个编程语言都要考虑的一个重要话题。在Go语言的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分。 + +在程序中总有一部分函数总是要求必须能够成功的运行。比如`strconv.Itoa`将整数转换为字符串,从数组或切片中读写元素,从`map`读取已经存在的元素等。这类操作在运行时几乎不会失败,除非程序中有BUG,或遇到灾难性的、不可预料的情况,比如运行时的内存溢出。如果真的遇到真正异常情况,我们只要简单终止程序就可以了。 + +排除异常的情况,如果程序运行失败仅被认为是几个预期的结果之一。对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,当从一个`map`查询一个结果时,可以通过额外的布尔值判断是否成功: + +```go +if v, ok := m["key"]; ok { + return v +} +``` + +但是导致失败的原因通常不止一种,很多时候用户希望了解更多的错误信息。如果只是用简单的布尔类型的状态值将不能满足这个要求。在C语言中,默认采用一个整数类型的`errno`来表达错误,这样就可以根据需要定义多种错误类型。在Go语言中,`syscall.Errno`就是对应C语言中`errno`类型的错误。在`syscall`包中的接口,如果有返回错误的话,底层也是`syscall.Errno`错误类型。 + +比如我们通过`syscall`包的接口来修改文件的模式时,如果遇到错误我们可以将`err`强制断言为`syscall.Errno`错误类型处理: + +```go +err := syscall.Chmod(":invalid path:", 0666) +if err != nil { + log.Fatal(err.(syscall.Errno)) +} +``` + +我们还可以进一步地通过类型查询或类型断言来获取底层真实的错误类型,这样就可以获取更详细的错误信息。不过一般情况下我们并不关心错误在底层的表达方式,我们只需要知道它是一个错误就可以了。当返回的错误值不是`nil`时,我们可以通过调用`error`接口类型的`Error`方法来获得字符串类型的错误信息。 + +在Go语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG或发生了其它不可控的问题。Go语言推荐使用`recover`函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。 + +如果某个接口简单地将所有普通的错误当做异常抛出,将会使错误信息杂乱且没有价值。就像在`main`函数中直接捕获全部一样,是没有意义的: + +```go +func main() { + defer func() { + if r := recover(); r != nil { + log.Fatal(r) + } + }() + + ... +} +``` + +捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式。 + +## 错误处理策略 + +让我们演示一个文件复制的例子:函数需要打开两个文件,然后将其中一个文件的内容复制到另一个文件: + +```go +func CopyFile(dstName, srcName string) (written int64, err error) { + src, err := os.Open(srcName) + if err != nil { + return + } + + dst, err := os.Create(dstName) + if err != nil { + return + } + + written, err = io.Copy(dst, src) + dst.Close() + src.Close() + return +} +``` + +上面的代码虽然能够工作,但是隐藏一个bug。如果第一个`os.Open`调用成功,但是第二个`os.Create`调用失败,那么会在没有释放`src`文件资源的情况下返回。虽然我们可以通过在第二个返回语句前添加`src.Close()`调用来修复这个BUG;但是当代码变得复杂时,类似的问题将很难被发现和修复。我们可以通过`defer`语句来确保每个被正常打开的文件都能被正常关闭: + +```go +func CopyFile(dstName, srcName string) (written int64, err error) { + src, err := os.Open(srcName) + if err != nil { + return + } + defer src.Close() + + dst, err := os.Create(dstName) + if err != nil { + return + } + defer dst.Close() + + return io.Copy(dst, src) +} +``` + +`defer`语句可以让我们在打开文件时马上思考如何关闭文件。不管函数如何返回,文件关闭语句始终会被执行。同时`defer`语句可以保证,即使`io.Copy`发生了异常,文件依然可以安全地关闭。 + +前文我们说到,Go语言中的导出函数一般不抛出异常,一个未受控的异常可以看作是程序的BUG。但是对于那些提供类似Web服务的框架而言;它们经常需要接入第三方的中间件。因为第三方的中间件是否存在BUG是否会抛出异常,Web框架本身是不能确定的。为了提高系统的稳定性,Web框架一般会通过`recover`来防御性地捕获所有处理流程中可能产生的异常,然后将异常转为普通的错误返回。 + +让我们以JSON解析器为例,说明recover的使用场景。考虑到JSON解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。 + +```go +func ParseJSON(input string) (s *Syntax, err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("JSON: internal error: %v", p) + } + }() + // ...parser... +} +``` + +在标准库中的`json`包,在内部递归解析JSON数据的时候如果遇到错误,会通过抛出异常的方式来快速跳出深度嵌套的函数调用,然后由最外一级的接口通过`recover`捕获`panic`,然后返回相应的错误信息。 + +Go语言库的实现习惯: 即使在包内部使用了`panic`,但是在导出函数时会被转化为明确的错误值。 + +# 获取错误的上下文 + +有时候为了方便上层用户理解;很多时候底层实现者会将底层的错误重新包装为新的错误类型返回给用户: + +```go +if _, err := html.Parse(resp.Body); err != nil { + return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) +} +``` + +上层用户在遇到错误时,可以很容易从业务层面理解错误发生的原因。但是鱼和熊掌总是很难兼得,在上层用户获得新的错误的同时,我们也丢失了底层最原始的错误类型(只剩下错误描述信息了)。 + +为了记录这种错误类型在包装的变迁过程中的信息,我们一般会定义一个辅助的`WrapError`函数,用于包装原始的错误,同时保留完整的原始错误类型。为了问题定位的方便,同时也为了能记录错误发生时的函数调用状态,我们很多时候希望在出现致命错误的时候保存完整的函数调用信息。同时,为了支持RPC等跨网络的传输,我们可能要需要将错误序列化为类似JSON格式的数据,然后再从这些数据中将错误解码恢复出来。 + +为此,我们可以定义自己的`github.com/chai2010/errors`包,里面是以下的错误类型: + +```go + +type Error interface { + Caller() []CallerInfo + Wraped() []error + Code() int + error + + private() +} + +type CallerInfo struct { + FuncName string + FileName string + FileLine int +} +``` + +其中`Error`为接口类型,是`error`接口类型的扩展,用于给错误增加调用栈信息,同时支持错误的多级嵌套包装,支持错误码格式。为了使用方便,我们可以定义以下的辅助函数: + +```go +func New(msg string) error +func NewWithCode(code int, msg string) error + +func Wrap(err error, msg string) error +func WrapWithCode(code int, err error, msg string) error + +func FromJson(json string) (Error, error) +func ToJson(err error) string +``` + +`New`用于构建新的错误类型,和标准库中`errors.New`功能类似,但是增加了出错误时的函数调用栈信息。`FromJson`用于从JSON字符串编码的错误中恢复错误对象。`NewWithCode`则是构造一个带错误码的错误,同时也包含出错误时的函数调用栈信息。`Wrap`和`WrapWithCode`则是错误二次包装函数,用于将底层的错误包装为新的错误,但是保留的原始的底层错误信息。这里返回的错误对象都可以直接调用`json.Marshal`将错误编码为JSON字符串。 + +我们可以这样使用包装函数: + +```go +import ( + "github.com/chai2010/errors" +) + +func loadConfig() error { + _, err := ioutil.ReadFile("/path/to/file") + if err != nil { + return errors.Wrap(err, "read failed") + } + + // ... +} + +func setup() error { + err := loadConfig() + if err != nil { + return errors.Wrap(err, "invalid config") + } + + // ... +} + +func main() { + if err := setup(); err != nil { + log.Fatal(err) + } + + // ... +} +``` + +上面的例子中,错误被进行了2层包装。我们可以这样遍历原始错误经历了哪些包装流程: + +```go + for i, e := range err.(errors.Error).Wraped() { + fmt.Printf("wraped(%d): %v\n", i, e) + } +``` + +同时也可以获取每个包装错误的函数调用堆栈信息: + +```go + for i, x := range err.(errors.Error).Caller() { + fmt.Printf("caller:%d: %s\n", i, x.FuncName) + } +``` + +如果需要将错误通过网络传输,可以用`errors.ToJson(err)`编码为JSON字符串: + +```go +// 以JSON字符串方式发送错误 +func sendError(ch chan<- string, err error) { + ch <- errors.ToJson(err) +} + +// 接收JSON字符串格式的错误 +func recvError(ch <-chan string) error { + p, err := errors.FromJson(<-ch) + if err != nil { + log.Fatal(err) + } + return p +} +``` + +对于基于http协议的网络服务,我们还可以给错误绑定一个对应的http状态码: + +```go +err := errors.NewWithCode(404, "http error code") + +fmt.Println(err) +fmt.Println(err.(errors.Error).Code()) +``` + +在Go语言中,错误处理也有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在`else`语句块中,而应直接放在函数体中。 + +```go +f, err := os.Open("filename.ext") +if err != nil { + // 失败的情形, 马上返回错误 +} + +// 正常的处理流程 +``` + +Go语言中大部分函数的代码结构几乎相同,首先是一系列的初始检查,用于防止错误发生,之后是函数的实际逻辑。 + + +# 错误的错误返回 + +Go语言中的错误是一种接口类型。接口信息中包含了原始类型和原始的值。只有当接口的类型和原始的值都为空的时候,接口的值才对应`nil`。其实当接口中类型为空的时候,原始值必然也是空的;反之,当接口对应的原始值为空的时候,接口对应的原始类型并不一定为空的。 + +在下面的例子中,试图返回自定义的错误类型,当没有错误的时候返回`nil`: + +```go +func returnsError() error { + var p *MyError = nil + if bad() { + p = ErrBad + } + return p // Will always return a non-nil error. +} +``` + +但是,最终返回的结果其实并非是`nil`:是一个正常的错误,错误的值是一个`MyError`类型的空指针。下面是改进的`returnsError`: + +```go +func returnsError() error { + if bad() { + return (*MyError)(err) + } + return nil +} +``` + +因此,在处理错误返回值的时候,没有错误的返回值最好直接写为`nil`。 + +Go语言作为一个强类型语言,不同类型之间必须要显式的转换(而且必须有相同的基础类型)。但是,Go语言中`interface`是一个例外:非接口类型到接口类型,或者是接口类型之间的转换都是隐式的。这是为了支持方便的鸭子面向对象编程,当然会牺牲一定的安全特性。 + +# 剖析异常 + +`panic`支持抛出任意类型的异常(而不仅仅是`error`类型的错误),`recover`函数调用的返回值和`panic`函数的输入参数类型一致,它们的函数签名如下: + +```go +func panic(interface{}) +func recover() interface{} +``` + +Go语言函数调用的正常流程是函数执行返回语句返回结果,在这个流程中是没有异常的,因此在这个流程中执行`recover`异常捕获函数始终是返回`nil`。另一种是异常流程: 当函数调用`panic`抛出异常,函数将停止执行后续的普通语句,但是之前注册的`defer`函数调用仍然保证会被正常执行,然后再返回到的调用者。对于当前函数的调用者,因为处理异常状态还没有被捕获,和直接调用`panic`函数的行为类似。在异常发生时,如果在`defer`中执行`recover`调用,它可以捕获触发`panic`时的参数,并且恢复到正常的执行流程。 + +在非`defer`语句中执行`recover`调用是初学者常犯的错误: + +```go +func main() { + if r := recover(); r != nil { + log.Fatal(r) + } + + panic(123) + + if r := recover(); r != nil { + log.Fatal(r) + } +} +``` + +上面程序中两个`recover`调用都不能捕获任何异常。在第一个`recover`调用执行时,函数必然是在正常的非异常执行流程中,这时候`recover`调用将返回`nil`。发生异常时,第二个`recover`调用将没有机会被执行到,因为`panic`调用会导致函数马上执行已经注册`defer`的函数后返回。 + +其实`recover`函数调用有着更严格的要求:我们必须在`defer`函数中直接调用`recover`。如果`defer`中调用的是`recover`函数的包装函数的话,异常的捕获工作将失败!比如,有时候我们可能希望包装自己的`MyRecover`函数,在内部增加必要的日志信息然后再调用`recover`,这是错误的做法: + +```go +func main() { + defer func() { + // 无法捕获异常 + if r := MyRecover(); r != nil { + fmt.Println(r) + } + }() + panic(1) +} + +func MyRecover() interface{} { + log.Println("trace...") + return recover() +} +``` + +同样,如果是在嵌套的`defer`函数中调用`recover`也将导致无法捕获异常: + +```go +func main() { + defer func() { + defer func() { + // 无法捕获异常 + if r := recover(); r != nil { + fmt.Println(r) + } + }() + }() + panic(1) +} +``` + +2层嵌套的`defer`函数中直接调用`recover`和1层`defer`函数中调用包装的`MyRecover`函数一样,都是经过了2个函数帧才到达真正的`recover`函数,这个时候Goroutine的对应上一级栈帧中已经没有异常信息。 + +如果我们直接在`defer`语句中调用`MyRecover`函数又可以正常工作了: + +```go +func MyRecover() interface{} { + return recover() +} + +func main() { + // 可以正常捕获异常 + defer MyRecover() + panic(1) +} +``` + +但是,如果`defer`语句直接调用`recover`函数,依然不能正常捕获异常: + +```go +func main() { + // 无法捕获异常 + defer recover() + panic(1) +} +``` + +必须要和有异常的栈帧只隔一个栈帧,`recover`函数才能正常捕获异常。换言之,`recover`函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层`defer`函数)! + +当然,为了避免`recover`调用者不能识别捕获到的异常, 应该避免用`nil`为参数抛出异常: + +```go +func main() { + defer func() { + if r := recover(); r != nil { ... } + // 虽然总是返回nil, 但是可以恢复异常状态 + }() + + // 警告: 用`nil`为参数抛出异常 + panic(nil) +} +``` + +当希望将捕获到的异常转为错误时,如果希望忠实返回原始的信息,需要针对不同的类型分别处理: + +```go +func foo() (err error) { + defer func() { + if r := recover(); r != nil { + switch x := r.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + err = fmt.Errorf("Unknown panic: %v", r) + } + } + }() + + panic("TODO") +} +``` + +基于这个代码模板,我们甚至可以模拟出不同类型的异常。通过为定义不同类型的保护接口,我们就可以区分异常的类型了: + +```go +func main { + defer func() { + if r := recover(); r != nil { + switch x := r.(type) { + case runtime.Error: + // 这是运行时错误类型异常 + case error: + // 普通错误类型异常 + default: + // 其他类型异常 + } + } + }() + + ... +} +``` + +不过这样做和Go语言简单直接的编程哲学背道而驰了。 diff --git a/ch1-basic/ch1-08-ext.md b/ch1-basic/ch1-08-ext.md index 48b69d8..6c994ff 100644 --- a/ch1-basic/ch1-08-ext.md +++ b/ch1-basic/ch1-08-ext.md @@ -1,4 +1,4 @@ -## 1.8. 补充说明 +## 1.8 补充说明 本书定位是Go语言进阶图书,因此读者需要有一定的Go语言基础。如果对Go语言不太了解,作者推荐通过以下资料开始学习Go语言。首先是安装Go语言环境,然后通过`go tool tour`命令打开“A Tour of Go”教程学习。在学习“A Tour of Go”教程的同时,可以阅读Go语言官方团队出版的[《The Go Programming Language》](http://www.gopl.io/)教程。[《The Go Programming Language》](http://www.gopl.io/)在国内Go语言社区被称为Go语言圣经,它将带你系统地学习Go语言。在学习的同时可以尝试用Go语言解决一些小问题,如果遇到要差异API的适合可以通过godoc命令打开自带的文档查询。Go语言本身不仅仅包含了所有的文档,也包含了所有标准库的实现代码,这是第一手的最权威的Go语言资料。我们此时你应该已经可以熟练使用Go语言了。 diff --git a/ch1-basic/readme.md b/ch1-basic/readme.md index 7d65331..56aa395 100644 --- a/ch1-basic/readme.md +++ b/ch1-basic/readme.md @@ -1,3 +1,3 @@ -# 第一章 语言基础 +# 第1章 语言基础 本章首先简要介绍Go语言的发展历史,并较详细地分析了“Hello World”程序在各个祖先语言中演化过程。然后,对以数组、字符串和切片为代表的基础结构,对以函数、方法和接口所体现的面向过程和鸭子对象的编程,以及Go语言特有的并发编程模型和错误处理哲学做了简单介绍。最后,针对macOS、Windows、Linux几个主流的开发平台,推荐了几个较友好的Go语言编辑器和集成开发环境,因为好的工具可以极大地提高我们的效率。 diff --git a/ch2-cgo/ch2-01-hello-cgo.md b/ch2-cgo/ch2-01-hello-cgo.md index 16474c0..b1589c5 100644 --- a/ch2-cgo/ch2-01-hello-cgo.md +++ b/ch2-cgo/ch2-01-hello-cgo.md @@ -1,4 +1,4 @@ -# 2.1. 快速入门 +# 2.1 快速入门 本节我们将通过由浅入深的一系列小例子来快速掌握CGO的基本用法。 diff --git a/ch2-cgo/ch2-02-basic.md b/ch2-cgo/ch2-02-basic.md index 6a6f45f..0484975 100644 --- a/ch2-cgo/ch2-02-basic.md +++ b/ch2-cgo/ch2-02-basic.md @@ -1,4 +1,4 @@ -# 2.2. CGO基础 +# 2.2 CGO基础 要使用CGO特性,需要安装C/C++构建工具链,在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。同时需要保证环境变量`CGO_ENABLED`被设置为1,这表示CGO是被启用的状态。在本地构建时`CGO_ENABLED`默认是启用的,当交叉构建时CGO默认是禁止的。比如要交叉构建ARM环境运行的Go程序,需要手工设置好C/C++交叉构建的工具链,同时开启`CGO_ENABLED`环境变量。然后通过`import "C"`语句启用CGO特性。 diff --git a/ch2-cgo/ch2-03-cgo-types.md b/ch2-cgo/ch2-03-cgo-types.md index 00f9269..795ee38 100644 --- a/ch2-cgo/ch2-03-cgo-types.md +++ b/ch2-cgo/ch2-03-cgo-types.md @@ -1,4 +1,4 @@ -# 2.3. 类型转换 +# 2.3 类型转换 最初CGO是为了达到方便从Go语言函数调用C语言函数以复用C语言资源这一目的而出现的(因为C语言还会涉及回调函数,自然也会涉及到从C语言函数调用Go语言函数)。现在,它已经演变为C语言和Go语言双向通讯的桥梁。要想利用好CGO特性,自然需要了解此二语言类型之间的转换规则,这是本节要讨论的问题。 diff --git a/ch2-cgo/ch2-04-func.md b/ch2-cgo/ch2-04-func.md index 20c1917..a3298ab 100644 --- a/ch2-cgo/ch2-04-func.md +++ b/ch2-cgo/ch2-04-func.md @@ -1,4 +1,4 @@ -# 2.4. 函数调用 +# 2.4 函数调用 函数是C语言编程的核心,通过CGO技术我们不仅仅可以在Go语言中调用C语言函数,也可以将Go语言函数导出为C语言函数。 diff --git a/ch2-cgo/ch2-05-internal.md b/ch2-cgo/ch2-05-internal.md index b0c0d6b..1d81697 100644 --- a/ch2-cgo/ch2-05-internal.md +++ b/ch2-cgo/ch2-05-internal.md @@ -1,4 +1,4 @@ -# 2.5. 内部机制 +# 2.5 内部机制 对于刚刚接触CGO用户来说,CGO的很多特性类似魔法。CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码。本节我们尝试从生成的代码分析Go语言和C语言函数直接相互调用的流程。 diff --git a/ch2-cgo/ch2-06-qsort.md b/ch2-cgo/ch2-06-qsort.md index bc9840b..6d0caca 100644 --- a/ch2-cgo/ch2-06-qsort.md +++ b/ch2-cgo/ch2-06-qsort.md @@ -1,4 +1,4 @@ -# 2.6. 实战: 封装qsort +# 2.6 实战: 封装qsort qsort快速排序函数是C语言的高阶函数,支持用于自定义排序比较函数,可以对任意类型的数组进行排序。本节我们尝试基于C语言的qsort函数封装一个Go语言版本的qsort函数。 diff --git a/ch2-cgo/ch2-07-memory.md b/ch2-cgo/ch2-07-memory.md index 1fd6bdc..c847fd5 100644 --- a/ch2-cgo/ch2-07-memory.md +++ b/ch2-cgo/ch2-07-memory.md @@ -1,4 +1,4 @@ -# 2.7. CGO内存模型 +# 2.7 CGO内存模型 CGO是架接Go语言和C语言的桥梁,它使二者在二进制接口层面实现了互通,但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的传递,则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的,但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针,那么以旧指针访问Go对象时会导致程序崩溃。 diff --git a/ch2-cgo/ch2-08-class.md b/ch2-cgo/ch2-08-class.md index c959270..5700fa4 100644 --- a/ch2-cgo/ch2-08-class.md +++ b/ch2-cgo/ch2-08-class.md @@ -1,4 +1,4 @@ -# 2.8. C++ 类包装 +# 2.8 C++ 类包装 CGO是C语言和Go语言之间的桥梁,原则上无法直接支持C++的类。CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。一个C++类的构造函数在编译为目标文件时如何生成链接符号名称、方法在不同平台甚至是C++的不同版本之间都是不一样的。但是C++是兼容C语言,所以我们可以通过增加一组C语言函数接口作为C++类和CGO之间的桥梁,这样就可以间接地实现C++和Go之间的互联。当然,因为CGO只支持C语言中值类型的数据类型,所以我们是无法直接使用C++的引用参数等特性的。 diff --git a/ch2-cgo/ch2-09-static-shared-lib.md b/ch2-cgo/ch2-09-static-shared-lib.md index 12c53b4..a6c16f1 100644 --- a/ch2-cgo/ch2-09-static-shared-lib.md +++ b/ch2-cgo/ch2-09-static-shared-lib.md @@ -1,4 +1,4 @@ -# 2.9. 静态库和动态库 +# 2.9 静态库和动态库 CGO在使用C/C++资源的时候一般有三种形式:直接使用源码;链接静态库;链接动态库。直接使用源码就是在`import "C"`之前的注释部分包含C代码,或者在当前包中包含C/C++源文件。链接静态库和动态库的方式比较类似,都是通过在LDFLAGS选项指定要链接的库方式链接。本节我们主要关注在CGO中如何使用静态库和动态库相关的问题。 diff --git a/ch2-cgo/ch2-10-link.md b/ch2-cgo/ch2-10-link.md index 76c51ca..e7fa1fc 100644 --- a/ch2-cgo/ch2-10-link.md +++ b/ch2-cgo/ch2-10-link.md @@ -1,4 +1,4 @@ -# 2.10. 编译和链接参数 +# 2.10 编译和链接参数 编译和链接参数是每一个C/C++程序员需要经常面对的问题。构建每一个C/C++应用均需要经过编译和链接两个步骤,CGO也是如此。 本节我们将简要讨论CGO中经常用到的编译和链接参数的用法。 diff --git a/ch2-cgo/ch2-11-ext.md b/ch2-cgo/ch2-11-ext.md index e5a672f..14709bc 100644 --- a/ch2-cgo/ch2-11-ext.md +++ b/ch2-cgo/ch2-11-ext.md @@ -1,4 +1,4 @@ -## 2.11. 补充说明 +## 2.11 补充说明 CGO是C语言和Go语言混合编程的技术,因此要想熟练地使用CGO需要了解这两门语言。C语言推荐两本书:第一本是C语言之父编写的《C程序设计语言》;第二本是讲述C语言模块化编程的《C语言接口与实现:创建可重用软件的技术》。Go语言推荐官方出版的《The Go Programming Language》和Go语言自带的全部文档和全部代码。 diff --git a/ch2-cgo/readme.md b/ch2-cgo/readme.md index 8f02c78..e749d20 100644 --- a/ch2-cgo/readme.md +++ b/ch2-cgo/readme.md @@ -1,3 +1,3 @@ -# 第二章 CGO编程 +# 第2章 CGO编程 C/C++经过几十年的发展,已经积累了庞大的软件资产,它们很多久经考验而且性能已经足够优化。Go语言必须能够站在C/C++这个巨人的肩膀之上,有了海量的C/C++软件资产兜底之后,我们才可以放心愉快地用Go语言编程。C语言作为一个通用语言,很多库会选择提供一个C兼容的API,然后用其他不同的编程语言实现。Go语言通过自带的一个叫CGO的工具来支持C语言函数调用,同时我们可以用Go语言导出C动态库接口给其它语言使用。本章主要讨论CGO编程中涉及的一些问题。 diff --git a/ch3-asm/ch3-01-basic.md b/ch3-asm/ch3-01-basic.md index 92ade1a..035be83 100644 --- a/ch3-asm/ch3-01-basic.md +++ b/ch3-asm/ch3-01-basic.md @@ -1,4 +1,4 @@ -# 3.1. 快速入门 +# 3.1 快速入门 Go汇编程序始终是幽灵一样的存在。我们将通过分析简单的Go程序输出的汇编代码,然后照猫画虎用汇编实现一个简单的输出程序。 diff --git a/ch3-asm/ch3-02-arch.md b/ch3-asm/ch3-02-arch.md index b70da7c..9381f38 100644 --- a/ch3-asm/ch3-02-arch.md +++ b/ch3-asm/ch3-02-arch.md @@ -1,4 +1,4 @@ -# 3.2. 计算机结构 +# 3.2 计算机结构 汇编语言是直面计算机的编程语言,因此理解计算机结构是掌握汇编语言的前提。当前流行的计算机基本采用的是冯·诺伊曼计算机体系结构(在某些特殊领域还有哈佛体系架构)。冯·诺依曼结构也称为普林斯顿结构,采用的是一种将程序指令和数据存储在一起的存储结构。冯·诺伊曼计算机中的指令和数据存储器其实指的是计算机中的内存,然后在配合CPU处理器就组成了一个最简单的计算机了。 diff --git a/ch3-asm/ch3-03-const-and-var.md b/ch3-asm/ch3-03-const-and-var.md index 681d220..e4d56dc 100644 --- a/ch3-asm/ch3-03-const-and-var.md +++ b/ch3-asm/ch3-03-const-and-var.md @@ -1,4 +1,4 @@ -# 3.3. 常量和全局变量 +# 3.3 常量和全局变量 程序中的一切变量的初始值都直接或间接地依赖常量或常量表达式生成。在Go语言中很多变量是默认零值初始化的,但是Go汇编中定义的变量最好还是手工通过常量初始化。有了常量之后,就可以衍生定义全局变量,并使用常量组成的表达式初始化其它各种变量。本节将简单讨论Go汇编语言中常量和全局变量的用法。 diff --git a/ch3-asm/ch3-04-func.md b/ch3-asm/ch3-04-func.md index bdf5156..7a13860 100644 --- a/ch3-asm/ch3-04-func.md +++ b/ch3-asm/ch3-04-func.md @@ -1,4 +1,4 @@ -# 3.4. 函数 +# 3.4 函数 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了。只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 diff --git a/ch3-asm/ch3-05-control-flow.md b/ch3-asm/ch3-05-control-flow.md index 618c2e3..4e5ac3f 100644 --- a/ch3-asm/ch3-05-control-flow.md +++ b/ch3-asm/ch3-05-control-flow.md @@ -1,4 +1,4 @@ -# 3.5. 控制流 +# 3.5 控制流 程序主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码。 diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index 26b9386..4ed92a6 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -1,4 +1,4 @@ -# 3.6. 再论函数 +# 3.6 再论函数 在前面的章节中我们已经简单讨论过Go的汇编函数,但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数,也就是栈的大小是可以预期的,叶子函数也就是可以基本忽略爆栈的问题(如果已经爆了,那也是上级函数的问题)。如果没有爆栈问题,那么也就是不会有栈的分裂问题;如果没有栈的分裂也就不需要移动栈上的指针,也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的,永远不用担心爆栈的风险。那么这些近似黑科技的特性是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。 diff --git a/ch3-asm/ch3-07-hack-asm.md b/ch3-asm/ch3-07-hack-asm.md index 0c4e8dc..5564556 100644 --- a/ch3-asm/ch3-07-hack-asm.md +++ b/ch3-asm/ch3-07-hack-asm.md @@ -1,4 +1,4 @@ -# 3.7. 汇编语言的威力 +# 3.7 汇编语言的威力 汇编语言的真正威力来自两个维度:一是突破框架限制,实现看似不可能的任务;二是突破指令限制,通过高级指令挖掘极致的性能。对于第一个问题,我们将演示如何通过Go汇编语言直接访问系统调用,和直接调用C语言函数。对于第二个问题,我们将演示X64指令中AVX等高级指令的简单用法。 diff --git a/ch3-asm/ch3-08-goroutine-id.md b/ch3-asm/ch3-08-goroutine-id.md index 42220d5..50360a6 100644 --- a/ch3-asm/ch3-08-goroutine-id.md +++ b/ch3-asm/ch3-08-goroutine-id.md @@ -1,4 +1,4 @@ -# 3.8. 例子:Goroutine ID +# 3.8 例子:Goroutine ID 在操作系统中,每个进程都会有一个唯一的进程编号,每个线程也有自己唯一的线程编号。同样在Go语言中,每个Goroutine也有自己唯一的Go程编号,这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号,但是Go语言却刻意没有提供获取该编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。 diff --git a/ch3-asm/ch3-09-debug.md b/ch3-asm/ch3-09-debug.md index 4fb37e0..b483088 100644 --- a/ch3-asm/ch3-09-debug.md +++ b/ch3-asm/ch3-09-debug.md @@ -1,4 +1,4 @@ -# 3.9. Delve调试器 +# 3.9 Delve调试器 目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具,LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持,而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。 diff --git a/ch3-asm/ch3-10-ext.md b/ch3-asm/ch3-10-ext.md index 9498e4c..f2d03c0 100644 --- a/ch3-asm/ch3-10-ext.md +++ b/ch3-asm/ch3-10-ext.md @@ -1,4 +1,4 @@ -## 3.10. 补充说明 +## 3.10 补充说明 如果是纯粹学习汇编语言,则可以从《深入理解程序设计:使用Linux汇编语言》开始,该书讲述了如何以C语言的思维变现汇编程序。如果是学习X86汇编,则可以从《汇编语言:基于x86处理器》一开始,然后再结合《现代x86汇编语言程序设计》学习AVX等高级汇编指令的使用。 diff --git a/ch3-asm/readme.md b/ch3-asm/readme.md index 202d669..b6de570 100644 --- a/ch3-asm/readme.md +++ b/ch3-asm/readme.md @@ -1,4 +1,4 @@ -# 第三章 Go汇编语言 +# 第3章 Go汇编语言 Go语言中很多设计思想和工具都是传承自Plan9操作系统,Go汇编语言也是基于Plan9汇编演化而来。根据Rob Pike的介绍,大神Ken Thompson在1986年为Plan9系统编写的C语言编译器输出的汇编伪代码就是Plan9汇编的前身。所谓的Plan9汇编语言只是便于以手工方式书写该C语言编译器输出的汇编伪代码而已。 diff --git a/ch4-rpc/ch4-01-rpc-intro.md b/ch4-rpc/ch4-01-rpc-intro.md index c046b19..8c8776a 100644 --- a/ch4-rpc/ch4-01-rpc-intro.md +++ b/ch4-rpc/ch4-01-rpc-intro.md @@ -1,4 +1,4 @@ -# 4.1. RPC入门 +# 4.1 RPC入门 RPC是远程过程调用的简称,是分布式系统中不同节点间流行的通信方式。在互联网时代,RPC已经和IPC一样成为一个不可或缺的基础构件。因此Go语言的标准库也提供了一个简单的RPC实现,我们将以此为入口学习RPC的各种用法。 diff --git a/ch4-rpc/ch4-02-pb-intro.md b/ch4-rpc/ch4-02-pb-intro.md index ce24b9f..bf0cd27 100644 --- a/ch4-rpc/ch4-02-pb-intro.md +++ b/ch4-rpc/ch4-02-pb-intro.md @@ -1,4 +1,4 @@ -# 4.2. Protobuf +# 4.2 Protobuf Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。 diff --git a/ch4-rpc/ch4-03-netrpc-hack.md b/ch4-rpc/ch4-03-netrpc-hack.md index 45861ec..195c0c1 100644 --- a/ch4-rpc/ch4-03-netrpc-hack.md +++ b/ch4-rpc/ch4-03-netrpc-hack.md @@ -1,4 +1,4 @@ -# 4.3. 玩转RPC +# 4.3 玩转RPC 在不同的场景中RPC有着不同的需求,因此开源的社区就诞生了各种RPC框架。本节我们将尝试Go内置RPC框架在一些比较特殊场景的用法。 diff --git a/ch4-rpc/ch4-04-grpc.md b/ch4-rpc/ch4-04-grpc.md index 7a060ef..54f57bb 100644 --- a/ch4-rpc/ch4-04-grpc.md +++ b/ch4-rpc/ch4-04-grpc.md @@ -1,4 +1,4 @@ -# 4.4. GRPC入门 +# 4.4 GRPC入门 GRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。GRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好。本节将讲述GRPC的简单用法。 diff --git a/ch4-rpc/ch4-05-grpc-hack.md b/ch4-rpc/ch4-05-grpc-hack.md index 6cec483..e72890e 100644 --- a/ch4-rpc/ch4-05-grpc-hack.md +++ b/ch4-rpc/ch4-05-grpc-hack.md @@ -1,4 +1,4 @@ -# 4.5. GRPC进阶 +# 4.5 GRPC进阶 作为一个基础的RPC框架,安全和扩展是经常遇到的问题。本节将简单介绍如何对GRPC进行安全认证。然后介绍通过GRPC的截取器特性,以及如何通过截取器优雅地实现Token认证、调用跟踪以及Panic捕获等特性。最后介绍了GRPC服务如何和其他Web服务共存。 diff --git a/ch4-rpc/ch4-06-grpc-ext.md b/ch4-rpc/ch4-06-grpc-ext.md index 139295a..356f4f7 100644 --- a/ch4-rpc/ch4-06-grpc-ext.md +++ b/ch4-rpc/ch4-06-grpc-ext.md @@ -1,4 +1,4 @@ -# 4.6. GRPC和Protobuf扩展 +# 4.6 GRPC和Protobuf扩展 目前开源社区已经围绕Protobuf和GRPC开发出众多扩展,形成了庞大的生态。本节我们将简单介绍验证器和REST接口扩展。 diff --git a/ch4-rpc/ch4-07-pbgo.md b/ch4-rpc/ch4-07-pbgo.md index 9f036e5..2504b65 100644 --- a/ch4-rpc/ch4-07-pbgo.md +++ b/ch4-rpc/ch4-07-pbgo.md @@ -1,4 +1,4 @@ -# 4.7. pbgo: 基于Protobuf的框架 +# 4.7 pbgo: 基于Protobuf的框架 pbgo是我们专门针对本节内容设计的较为完整的迷你框架,它基于Protobuf的扩展语法,通过插件自动生成rpc和rest相关代码。在本章第二节我们已经展示过如何定制一个Protobuf代码生成插件,并生成了rpc部分的代码。在本节我们将重点讲述pbgo中和Protobuf扩展语法相关的rest部分的工作原理。 diff --git a/ch4-rpc/ch4-08-ext.md b/ch4-rpc/ch4-08-ext.md index 3e1b746..04012f2 100644 --- a/ch4-rpc/ch4-08-ext.md +++ b/ch4-rpc/ch4-08-ext.md @@ -1,4 +1,4 @@ -## 4.8. 补充说明 +## 4.8 补充说明 目前专门讲述RPC的图书比较少。目前Protobuf和GRPC的官网都提供了详细的参考资料和例子。本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。 diff --git a/ch4-rpc/readme.md b/ch4-rpc/readme.md index 6d8a894..d6c58a5 100644 --- a/ch4-rpc/readme.md +++ b/ch4-rpc/readme.md @@ -1,3 +1,3 @@ -# 第四章 RPC和Protobuf +# 第4章 RPC和Protobuf RPC是远程过程调用的缩写(Remote Procedure Call),通俗地说就是调用远处的一个函数。远处到底有多远呢?可能是同一个文件内的不同函数,也可能是同一个机器的另一个进程的函数,还可能是远在火星好奇号上面的某个秘密方法。因为RPC涉及的函数可能非常之远,远到它们之间说着完全不同的语言,语言就成了两边的沟通障碍。而Protobuf因为支持多种不同的语言(甚至不支持的语言也可以扩展支持),其本身特性也非常方便描述服务的接口(也就是方法列表),因此非常适合作为RPC世界的接口交流语言。本章将讨论RPC的基本用法,如何针对不同场景设计自己的RPC服务,以及围绕Protobuf构造的更为庞大的RPC生态。 diff --git a/ch5-web/ch5-01-introduction.md b/ch5-web/ch5-01-introduction.md index 4a41464..9bbde9c 100644 --- a/ch5-web/ch5-01-introduction.md +++ b/ch5-web/ch5-01-introduction.md @@ -1,4 +1,4 @@ -# 5.1. web 开发简介 +# 5.1 web 开发简介 由于 golang 的 `net/http` 提供了基础的路由函数组合,并且也提供了丰富的功能函数。所以在 golang 社区里有一种观点认为用 golang 写 api 不需要框架。其看法也存在一定的道理,如果你的项目路由在个位数,URI 固定且不通过 URI 来传递参数,那么使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力不从心。例如下面这样的路由: diff --git a/ch5-web/ch5-02-router.md b/ch5-web/ch5-02-router.md index 532560c..3a3eda7 100644 --- a/ch5-web/ch5-02-router.md +++ b/ch5-web/ch5-02-router.md @@ -1,4 +1,4 @@ -# 5.2. router 请求路由 +# 5.2 router 请求路由 在常见的 web 框架中,router 是必备的组件。golang 圈子里 router 也时常被称为 http 的 multiplexer。在上一节中我们通过对 Burrow 代码的简单学习,已经知道如何用 http 标准库中内置的 mux 来完成简单的路由功能了。如果开发 web 系统对路径中带参数没什么兴趣的话,用 http 标准库中的 mux 就可以。 diff --git a/ch5-web/ch5-03-middleware.md b/ch5-web/ch5-03-middleware.md index 7dd0338..2f328e1 100644 --- a/ch5-web/ch5-03-middleware.md +++ b/ch5-web/ch5-03-middleware.md @@ -1,4 +1,4 @@ -# 5.3. middleware 中间件 +# 5.3 middleware 中间件 本章将对现在流行的 web 框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。 diff --git a/ch5-web/ch5-04-validator.md b/ch5-web/ch5-04-validator.md index bb3c054..dc8a12b 100644 --- a/ch5-web/ch5-04-validator.md +++ b/ch5-web/ch5-04-validator.md @@ -1,4 +1,4 @@ -# 5.4. validator 请求校验 +# 5.4 validator 请求校验 社区里曾经有人用这张图来嘲笑 PHP: diff --git a/ch5-web/ch5-05-database.md b/ch5-web/ch5-05-database.md index 8ae1d49..62cfdf0 100644 --- a/ch5-web/ch5-05-database.md +++ b/ch5-web/ch5-05-database.md @@ -1,4 +1,4 @@ -# 5.5. Database 和数据库打交道 +# 5.5 Database 和数据库打交道 本节将对 db/sql 官方标准库作一些简单分析,并介绍一些应用比较广泛的开源 orm 和 sql builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。 diff --git a/ch5-web/ch5-06-ratelimit.md b/ch5-web/ch5-06-ratelimit.md index 2eb1cf1..5d665b7 100644 --- a/ch5-web/ch5-06-ratelimit.md +++ b/ch5-web/ch5-06-ratelimit.md @@ -1,4 +1,4 @@ -# 5.6. Ratelimit 服务流量限制 +# 5.6 Ratelimit 服务流量限制 计算机程序可依据其瓶颈分为 Disk IO-bound,CPU-bound,Network-bound,分布式场景下有时候也会外部系统而导致自身瓶颈。 diff --git a/ch5-web/ch5-07-layout-of-web-project.md b/ch5-web/ch5-07-layout-of-web-project.md index 78d3ebe..3b85668 100644 --- a/ch5-web/ch5-07-layout-of-web-project.md +++ b/ch5-web/ch5-07-layout-of-web-project.md @@ -1,4 +1,4 @@ -# 5.7. layout 常见大型 web 项目分层 +# 5.7 layout 常见大型 web 项目分层 流行的 web 框架大多数是 MVC 框架,MVC 这个概念最早由 Trygve Reenskaug 在 1978 年提出,为了能够对 GUI 类型的应用进行方便扩展,将程序划分为: diff --git a/ch5-web/ch5-08-interface-and-web.md b/ch5-web/ch5-08-interface-and-web.md index 99fc133..fc76121 100644 --- a/ch5-web/ch5-08-interface-and-web.md +++ b/ch5-web/ch5-08-interface-and-web.md @@ -1,4 +1,4 @@ -# 5.8. interface 和 table-driven 开发 +# 5.8 interface 和 table-driven 开发 在 web 项目中经常会遇到外部依赖环境的变化,比如: diff --git a/ch5-web/ch5-09-gated-launch.md b/ch5-web/ch5-09-gated-launch.md index e4fd872..7dd3ad3 100644 --- a/ch5-web/ch5-09-gated-launch.md +++ b/ch5-web/ch5-09-gated-launch.md @@ -1,4 +1,4 @@ -# 5.9. 灰度发布和 A/B test +# 5.9 灰度发布和 A/B test 中型的互联网公司往往有着以百万计的用户,而大型互联网公司的系统则可能要服务千万级甚至亿级的用户需求。大型系统的请求流入往往是源源不断的,任何风吹草动,都一定会有最终用户感受得到。例如你的系统在上线途中会拒绝一些上游过来的请求,而这时候依赖你的系统没有做任何容错,那么这个错误就会一直向上抛出,直到触达最终用户。形成一次对用户切切实实的伤害。这种伤害可能是在用户的 app 上弹出一个让用户摸不着头脑的诡异字符串,用户只要刷新一下页面就可以忘记这件事。但也可能会让正在心急如焚地和几万竞争对手同时抢夺秒杀商品的用户,因为代码上的小问题,丧失掉了先发优势,与自己蹲了几个月的心仪产品失之交臂。对用户的伤害有多大,取决于你的系统对于你的用户来说有多重要。 @@ -34,9 +34,9 @@ │┌─────┐│ │┌─────┐│ │┌─────┐│ │┌─────┐│ │└─────┘│ │└─────┘│ │└─────┘│ │└─────┘│ └───────┘ └───────┘ └───────┘ └───────┘ - - - group1 group2 group3 group4 + + + group1 group2 group3 group4 ``` @@ -105,33 +105,33 @@ func isTrue(phone string) bool { 这种情况可以按照指定的百分比,返回对应的 true 和 false,和上面的单纯按照概率的区别是这里我们需要调用方提供给我们一个输入参数,我们以该输入参数作为源来计算哈希,并以哈希后的结果来求模,并返回结果。这样可以保证同一个用户的返回结果多次调用是一致的,在下面这种场景下,必须使用这种结果可预期的灰度算法: ```shell - .---------. - ( user_2 ) - `---------' - +--------+ - .---------. | set.V2 |---------------------+ - ( user_1 ) +--------+ | - `---------' | | - +--------+ | | - +--------------| set.V2 | | | - | +--------+ | | - | | | | - v | | v + .---------. + ( user_2 ) + `---------' + +--------+ + .---------. | set.V2 |---------------------+ + ( user_1 ) +--------+ | + `---------' | | + +--------+ | | + +--------------| set.V2 | | | + | +--------+ | | + | | | | + v | | v +-------------+ | | +-------------+ | storage_v1 | | | | storage_v2 | +-------------+ | | +-------------+ - | | | | - | | | | - | | | | - | v | | - | +--------+ | | - +------------->| get.V2 | | | - +--------+ | | - | | - v | - +--------+ | - | get.V2 |<--------------------+ - +--------+ + | | | | + | | | | + | | | | + | v | | + | +--------+ | | + +------------->| get.V2 | | | + +--------+ | | + | | + v | + +--------+ | + | get.V2 |<--------------------+ + +--------+ ``` ## 如何实现一套灰度发布系统 diff --git a/ch5-web/ch5-10-service-discovery.md b/ch5-web/ch5-10-service-discovery.md index 1692eb8..85622ba 100644 --- a/ch5-web/ch5-10-service-discovery.md +++ b/ch5-web/ch5-10-service-discovery.md @@ -1,4 +1,4 @@ -# 5.10. Service Discovery 服务发现 +# 5.10 Service Discovery 服务发现 在微服务架构中,服务之间是存在依赖的。例如在订单系统中创建订单时,需要对用户信息做快照,这时候也就意味着这个流程要依赖: 订单、用户两个系统。当前大型网站的语境下,多服务分布式共存,单个服务也可能会跑在多台物理/虚拟机上。所以即使你知道你需要依赖的是“订单服务”这个具体的服务,实际面对的仍然是多个 ip+port 组成的集群。因此你需要: 1. 通过“订单服务”这个名字找到它对应的 ip+port 列表;2. 决定把这个请求发到哪一个 ip+port 上的订单服务。 @@ -19,25 +19,25 @@ ip+port 的组合往往被称为 endpoint。通过“订单服务”去找到这 不过使用公共的 dns 服务也存在问题,我们的 dns 服务会变成整个服务的集中的那个中心点,这样会给整个分布式系统带来一定的风险。一旦 dns 服务挂了,那么我们也就找不到自己的依赖了。我们可以使用自己本地的缓存来缓解这个问题。比如某个服务最近访问过下游服务,那么可以将下游的 ip+port 缓存在本地,如果 dns 服务挂掉了,那我至少可以用本地的缓存做个兜底,不至于什么都找不到。 ``` - ┌────────────────┐ .─────────────────. + ┌────────────────┐ .─────────────────. │ My Service │─────────▶( local dns cache ) - └────────────────┘ `─────────────────' - │ - │ - │ - ┌───────── X ──────────┤ - │ │ - │ │ - │ │ - ▼ │ - .─────────────────. │ -( dns service ) │ - `─────────────────' │ - │ - ▼ - ┌────────────────────────┐ - │ dependent service │ - └────────────────────────┘ + └────────────────┘ `─────────────────' + │ + │ + │ + ┌───────── X ──────────┤ + │ │ + │ │ + │ │ + ▼ │ + .─────────────────. │ +( dns service ) │ + `─────────────────' │ + │ + ▼ + ┌────────────────────────┐ + │ dependent service │ + └────────────────────────┘ ``` 服务名和 endpoints 的对应也很直观,无非 `字符串` -> `endpoint 列表`。 diff --git a/ch5-web/ch5-11-load-balance.md b/ch5-web/ch5-11-load-balance.md index 20d198e..0b9c038 100644 --- a/ch5-web/ch5-11-load-balance.md +++ b/ch5-web/ch5-11-load-balance.md @@ -1,4 +1,4 @@ -# 5.11. Load-Balance 负载均衡 +# 5.11 Load-Balance 负载均衡 本节将会讨论常见的 web 后端服务之间的负载均衡手段。 diff --git a/ch5-web/ch5-12-dist-config.md b/ch5-web/ch5-12-dist-config.md index 3c6662d..2643d29 100644 --- a/ch5-web/ch5-12-dist-config.md +++ b/ch5-web/ch5-12-dist-config.md @@ -1 +1 @@ -# 5.12. Dist-config 分布式配置服务 +# 5.12 Dist-config 分布式配置服务 diff --git a/ch5-web/ch5-13-circuit-breaker.md b/ch5-web/ch5-13-circuit-breaker.md index 427d780..e0ba2ae 100644 --- a/ch5-web/ch5-13-circuit-breaker.md +++ b/ch5-web/ch5-13-circuit-breaker.md @@ -1 +1 @@ -# 5.13. Circuit-Breaker 熔断保护 +# 5.13 Circuit-Breaker 熔断保护 diff --git a/ch5-web/ch5-14-monitor.md b/ch5-web/ch5-14-monitor.md index 1707c15..e1a72d7 100644 --- a/ch5-web/ch5-14-monitor.md +++ b/ch5-web/ch5-14-monitor.md @@ -1 +1 @@ -# 5.8. Monitor metrics 和服务监控 +# 5.14 Monitor metrics 和服务监控 diff --git a/ch5-web/readme.md b/ch5-web/readme.md index 3707858..4b3e7a9 100644 --- a/ch5-web/readme.md +++ b/ch5-web/readme.md @@ -1,4 +1,4 @@ -# 第五章 go 和 web +# 第5章 go 和 web 本章将会阐述 go 在 web 开发方面的现状,并以几个典型的开源 web 框架为例,带大家深入 web 框架本身的执行流程。 diff --git a/ch6-cloud/ch6-01-cloud.md b/ch6-cloud/ch6-01-cloud.md index a3e5bc4..4f02fd8 100644 --- a/ch6-cloud/ch6-01-cloud.md +++ b/ch6-cloud/ch6-01-cloud.md @@ -1,4 +1,4 @@ -# 6.1. 云上地鼠 +# 6.1 云上地鼠 从 2014 年 docker 诞生开始,Go 语言开始在分布式领域崭露头角,之后 k8s,kong,groupcache,nats,etcd,tidb 让人们不断看到了 Go 在分布式领域的新的可能性。目前在大部分的分布式场景下,Go 语言都有相应的产品,无论是存储,rpc 框架,消息队列等等。Go 被称为分布式时代的 C 语言着实所言非虚。 diff --git a/ch6-cloud/ch6-02-search-engine.md b/ch6-cloud/ch6-02-search-engine.md index 89e1d70..b6aa1b0 100644 --- a/ch6-cloud/ch6-02-search-engine.md +++ b/ch6-cloud/ch6-02-search-engine.md @@ -1,4 +1,4 @@ -# 6.2. 分布式搜索引擎 +# 6.2 分布式搜索引擎 在 web 一章中,我们提到 MySQL 很脆弱。数据库系统本身要保证实时和强一致性,所以其功能设计上都是为了满足这种一致性需求。比如 write ahead log 的设计,基于 B+ 树实现的索引和数据组织,以及基于 MVCC 实现的事务等等。 @@ -27,21 +27,21 @@ elasticsearch 是开源分布式搜索引擎的霸主,其依赖于 Lucene 实 虽然 es 是针对搜索场景来订制的,但如前文所言,实际应用中常常用 es 来作为 database 来使用,就是因为倒排列表的特性。可以用比较朴素的观点来理解倒排索引: ``` -┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┐ -│ order_id: 103 │──────▶│ doc_id:4231 │ doc_id:4333 │ doc_id:5123 │ doc_id:9999 │ -└─────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────┘ - - - - - -┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐ -│ sku_id: 30221 │──────▶│ doc_id:4231 │ doc_id:5123 │ doc_id:5644 │ doc_id:7801 │ doc_id:9999 │ -└─────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘ - - - - +┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┐ +│ order_id: 103 │──────▶│ doc_id:4231 │ doc_id:4333 │ doc_id:5123 │ doc_id:9999 │ +└─────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────┘ + + + + + +┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐ +│ sku_id: 30221 │──────▶│ doc_id:4231 │ doc_id:5123 │ doc_id:5644 │ doc_id:7801 │ doc_id:9999 │ +└─────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘ + + + + ┌─────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐ │ city_id: 3 │──────▶│ doc_id:5123 │ doc_id:9999 │doc_id:10232 │doc_id:54321 │doc_id:63142 │doc_id:71230 │doc_id:90123 │ └─────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘ @@ -371,23 +371,23 @@ SQL 的 where 部分就是 boolean expression。我们之前提到过,这种 b ### 通过时间戳进行增量数据同步 ``` - ┌────────────────────────┐ ┌────────────────────────┐ - │ move 10 min data to es │ │ move 10 min data to es │ - └────────────────────────┘ └────────────────────────┘ - + ┌────────────────────────┐ ┌────────────────────────┐ + │ move 10 min data to es │ │ move 10 min data to es │ + └────────────────────────┘ └────────────────────────┘ + │ │ ┌───────────────┐ ───────────────┼────────────────┬──────────────┴─────────────┬──────────────▶ │ time passes │ │ ┌───────┐ │ │ └───────────────┘ - │◀──┤ 10min ├───▶│ ┌────────────────────────┐ - │ └───────┘ │ │ move 10 min data to es │ - │ └────────────────────────┘ - │ - │ - │ - │ - ┌────────────────────────┐ - │ move 10 min data to es │ - └────────────────────────┘ + │◀──┤ 10min ├───▶│ ┌────────────────────────┐ + │ └───────┘ │ │ move 10 min data to es │ + │ └────────────────────────┘ + │ + │ + │ + │ + ┌────────────────────────┐ + │ move 10 min data to es │ + └────────────────────────┘ ``` 这种同步方式与业务强绑定,例如 wms 系统中的出库单,我们并不需要非常实时,稍微有延迟也可以接受,那么我们可以每分钟从 MySQL 的出库单表中,把最近十分钟创建的所有出库单取出,批量存入 es 中,具体的逻辑实际上就是一条 SQL: @@ -407,48 +407,48 @@ select * from wms_orders where update_time >= date_sub(now(), interval 11 minute ### 通过 binlog 进行数据同步 ``` - ┌────────────────────────┐ - │ MySQL master │ - └────────────────────────┘ - │ - │ - │ - │ - │ - │ - ▼ - ┌───────────────────┐ - │ row format binlog │ - └───────────────────┘ - │ - │ - │ - ┌───────────────┴──────────────┐ - │ │ - │ │ - ▼ ▼ -┌────────────────────────┐ ┌─────────────────┐ -│ MySQL slave │ │ canal │ -└────────────────────────┘ └─────────────────┘ - │ - ┌─────────┴──────────┐ - │ parsed binlog │ - └─────────┬──────────┘ - │ - ▼ - ┌────────────────┐ - │ kafka │─────┐ - └────────────────┘ │ - │ - │ - │ - │ - ┌───────────┴──────┐ - │ kafka consumer │ - └───────────┬──────┘ - │ - │ - │ + ┌────────────────────────┐ + │ MySQL master │ + └────────────────────────┘ + │ + │ + │ + │ + │ + │ + ▼ + ┌───────────────────┐ + │ row format binlog │ + └───────────────────┘ + │ + │ + │ + ┌───────────────┴──────────────┐ + │ │ + │ │ + ▼ ▼ +┌────────────────────────┐ ┌─────────────────┐ +│ MySQL slave │ │ canal │ +└────────────────────────┘ └─────────────────┘ + │ + ┌─────────┴──────────┐ + │ parsed binlog │ + └─────────┬──────────┘ + │ + ▼ + ┌────────────────┐ + │ kafka │─────┐ + └────────────────┘ │ + │ + │ + │ + │ + ┌───────────┴──────┐ + │ kafka consumer │ + └───────────┬──────┘ + │ + │ + │ │ ┌────────────────┐ └─────▶│ elasticsearch │ └────────────────┘ diff --git a/ch6-cloud/ch6-03-raft.md b/ch6-cloud/ch6-03-raft.md index bea568b..9dd9620 100644 --- a/ch6-cloud/ch6-03-raft.md +++ b/ch6-cloud/ch6-03-raft.md @@ -1,4 +1,4 @@ -# 6.3. Raft协议 +# 6.3 Raft协议 raft 是一种分布式一致性算法,其能够保证在 2n+1 的系统,有 n+1 以上的节点存活时,向集群中写入的数据保证不会丢失。相比 paxos,其有更好的易读性和简洁性,所以从诞生起便受到很多人的赞许。该算法与 paxos 类似,被广泛应用于分布式调度的元信息存储,或在存储领域进行日志复制。 diff --git a/ch6-cloud/ch6-04-queue.md b/ch6-cloud/ch6-04-queue.md index cf475fe..0fbe31b 100644 --- a/ch6-cloud/ch6-04-queue.md +++ b/ch6-cloud/ch6-04-queue.md @@ -1,3 +1,3 @@ -# 6.4. 分布式队列 +# 6.4 分布式队列 TODO diff --git a/ch6-cloud/ch6-05-cache.md b/ch6-cloud/ch6-05-cache.md index deee771..34ff65b 100644 --- a/ch6-cloud/ch6-05-cache.md +++ b/ch6-cloud/ch6-05-cache.md @@ -1,3 +1,3 @@ -# 6.5. 分布式缓存 +# 6.5 分布式缓存 TODO diff --git a/ch6-cloud/ch6-06-etcd.md b/ch6-cloud/ch6-06-etcd.md index 5dd9489..63c086f 100644 --- a/ch6-cloud/ch6-06-etcd.md +++ b/ch6-cloud/ch6-06-etcd.md @@ -1,3 +1,3 @@ -# 6.6. etcd +# 6.6 etcd TODO diff --git a/ch6-cloud/ch6-07-dist-id.md b/ch6-cloud/ch6-07-dist-id.md index 9cf523a..0d63e45 100644 --- a/ch6-cloud/ch6-07-dist-id.md +++ b/ch6-cloud/ch6-07-dist-id.md @@ -1,4 +1,4 @@ -# 6.7. 分布式 id 生成器 +# 6.7 分布式 id 生成器 有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 id。以支持业务中的高并发场景。比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒 10w+。明星出轨时,会有大量热情的粉丝发微薄以表心意,同样产生短时间大量的消息。 @@ -7,27 +7,27 @@ Twitter 的 snowflake 算法是这种场景下的一个典型解法。先来看看 snowflake 是怎么一回事: ``` - - datacenter_id sequence_id - unused - │ │ - │ │ │ - │ │ │ - │ │ │ │ │ - │ │ │ │ │ - ▼ │◀────────────────── 41 bits ────────────────────▶│ ▼ ▼ + + datacenter_id sequence_id + unused + │ │ + │ │ │ + │ │ │ + │ │ │ │ │ + │ │ │ │ │ + ▼ │◀────────────────── 41 bits ────────────────────▶│ ▼ ▼ ┌─────┼──────────────────────────────────────────────────────┼────────┬────────┬────────────────┐ │ 0 │ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0 │ 00000 │ 00000 │ 0000 0000 0000 │ └─────┴──────────────────────────────────────────────────────┴────────┴────────┴────────────────┘ - ▲ ▲ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - - time in milliseconds worker_id + ▲ ▲ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + + time in milliseconds worker_id ``` @@ -104,7 +104,7 @@ func main() { ``` -当然,这个库也给我们留好了定制的后路: +当然,这个库也给我们留好了定制的后路: ```go // Epoch is set to the twitter snowflake epoch of Nov 04 2010 01:42:54 UTC @@ -222,4 +222,4 @@ func main() { fmt.Println(id) } -``` \ No newline at end of file +``` diff --git a/ch6-cloud/ch6-08-lock.md b/ch6-cloud/ch6-08-lock.md index 87b3997..6779330 100644 --- a/ch6-cloud/ch6-08-lock.md +++ b/ch6-cloud/ch6-08-lock.md @@ -1,4 +1,4 @@ -# 6.8. 分布式锁 +# 6.8 分布式锁 在单机程序并发或并行修改全局变量时,需要对修改行为加锁以创造临界区。为什么需要加锁呢?可以看看下段代码: diff --git a/ch6-cloud/ch6-09-scrawler.md b/ch6-cloud/ch6-09-scrawler.md index 0627867..09537fe 100644 --- a/ch6-cloud/ch6-09-scrawler.md +++ b/ch6-cloud/ch6-09-scrawler.md @@ -1,4 +1,4 @@ -# 6.9. 分布式爬虫 +# 6.9 分布式爬虫 ## 基于 colly 的单机爬虫 diff --git a/ch6-cloud/ch6-10-delay-job.md b/ch6-cloud/ch6-10-delay-job.md index e113e7a..17d4780 100644 --- a/ch6-cloud/ch6-10-delay-job.md +++ b/ch6-cloud/ch6-10-delay-job.md @@ -1,4 +1,4 @@ -# 6.10. 延时任务系统 +# 6.10 延时任务系统 我们在做系统时,很多时候是处理实时的任务,请求来了马上就处理,然后立刻给用户以反馈。但有时也会遇到非实时的任务,比如确定的时间点发布重要公告。或者需要在用户做了一件事情的 X 分钟/Y 小时后,对其特殊动作,比如通知、发券等等。 @@ -20,32 +20,32 @@ timer 的实现在工业界已经是有解的问题了。常见的就是时间 最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树: ``` - ┌─────┐ - │ │ - │ 5 │ - └─────┘ - │ - │ - ┌──────────┴──────────┐ - │ │ - ▼ ▼ - ┌─────┐ ┌─────┐ - │ │ │ │ - │ 6 │ │ 10 │ - └─────┘ └─────┘ - │ │ - ┌────┴─────┐ ┌────┴─────┐ - │ │ │ │ - ▼ ▼ ▼ ▼ - ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ - │ │ │ │ │ │ │ │ - │ 7 │ │ 6 │ │ 11 │ │ 20 │ - └─────┘ └─────┘ └─────┘ └─────┘ - │ │ - │ │ - ┌───────┴────┐ └───────┐ - │ │ │ - ▼ ▼ ▼ + ┌─────┐ + │ │ + │ 5 │ + └─────┘ + │ + │ + ┌──────────┴──────────┐ + │ │ + ▼ ▼ + ┌─────┐ ┌─────┐ + │ │ │ │ + │ 6 │ │ 10 │ + └─────┘ └─────┘ + │ │ + ┌────┴─────┐ ┌────┴─────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ + ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ + │ │ │ │ │ │ │ │ + │ 7 │ │ 6 │ │ 11 │ │ 20 │ + └─────┘ └─────┘ └─────┘ └─────┘ + │ │ + │ │ + ┌───────┴────┐ └───────┐ + │ │ │ + ▼ ▼ ▼ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ │ ............... │ │ │ 15 │ │ 8 │ │ 30 │ @@ -59,32 +59,32 @@ timer 的实现在工业界已经是有解的问题了。常见的就是时间 Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样: ``` - +-----+ - | | - | 0 | - +-----+ - | - | - | - v - +-----+-----+-----+-----+ - | | | | | - | 3 | 2 | 2 | 10 | - +-----+-----+-----+-----+ - | | | | - | | | | - +----------+ | | | | - +----------------+ 4*i+1 +-----------------------+ | | +-----------------------------+ - | +----------+ +-------------------+ +---+ | - | | | | - | | | | - v | | v + +-----+ + | | + | 0 | + +-----+ + | + | + | + v + +-----+-----+-----+-----+ + | | | | | + | 3 | 2 | 2 | 10 | + +-----+-----+-----+-----+ + | | | | + | | | | + +----------+ | | | | + +----------------+ 4*i+1 +-----------------------+ | | +-----------------------------+ + | +----------+ +-------------------+ +---+ | + | | | | + | | | | + v | | v +-----+-----+-----+-----+ | | +-----+-----+-----+-----+ | | | | | v v | | | | | | 20 | 4 | 5 | 13 | +-----+-----+-----+-----+ +-----+-----+-----+-----+ | 99 | 13 | 11 | 12 | +-----+-----+-----+-----+ | | | | | | | | | | +-----+-----+-----+-----+ - | 12 | 14 | 15 | 16 | | 3 | 10 | 3 | 3 | - +-----+-----+-----+-----+ +-----+-----+-----+-----+ + | 12 | 14 | 15 | 16 | | 3 | 10 | 3 | 3 | + +-----+-----+-----+-----+ +-----+-----+-----+-----+ ``` 小顶堆的性质,父节点比其 4 个子节点都小,子节点之间没有特别的大小关系要求。 @@ -127,22 +127,22 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉 我们可以参考 elasticsearch 的设计,每份任务数据都有多个副本,这里假设两副本: ``` -┌──────────┐ -│ node 1 │ +┌──────────┐ +│ node 1 │ ├──────────┴────────────────────────┐ │ ┏━━━┓ ┌───┐ ┌───┐ ┏━━━┓ │ │ ┃ 0 ┃ │ 1 │ │ 3 │ ┃ 4 ┃ │ │ ┗━━━┛ └───┘ └───┘ ┗━━━┛ │ └───────────────────────────────────┘ -┌──────────┐ -│ node 2 │ +┌──────────┐ +│ node 2 │ ├──────────┴────────────────────────┐ │ ┌───┐ ┏━━━┓ ┏━━━┓ │ │ │ 0 │ ┃ 2 ┃ ┃ 3 ┃ │ │ └───┘ ┗━━━┛ ┗━━━┛ │ └───────────────────────────────────┘ -┌──────────┐ -│ node 3 │ +┌──────────┐ +│ node 3 │ ├──────────┴────────────────────────┐ │ ┏━━━┓ ┌───┐ ┌───┐ │ │ ┃ 1 ┃ │ 2 │ │ 4 │ │ @@ -157,22 +157,22 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉 当有机器故障时,任务数据需要进行 rebalance 工作,比如 node 1 挂了: ``` -┌──────────┐ -│ node 1 │ +┌──────────┐ +│ node 1 │ ├──────────┴────────────────────────┐ │ │ │ X X X │ │ │ └───────────────────────────────────┘ -┌──────────┐ -│ node 2 │ +┌──────────┐ +│ node 2 │ ├──────────┴────────────────────────┐ │ ┌───┐ ┌───┐ ┏━━━┓ ┏━━━┓ ┏━━━┓ │ │ │ 0 │ │ 1 │ ┃ 2 ┃ ┃ 3 ┃ ┃ 4 ┃ │ │ └───┘ └───┘ ┗━━━┛ ┗━━━┛ ┗━━━┛ │ └───────────────────────────────────┘ -┌──────────┐ -│ node 3 │ +┌──────────┐ +│ node 3 │ ├──────────┴────────────────────────┐ │ ┏━━━┓ ┏━━━┓ ┌───┐ ┌───┐ ┌───┐ │ │ ┃ 0 ┃ ┃ 1 ┃ │ 2 │ │ 3 │ │ 4 │ │ diff --git a/ch6-cloud/ch6-11-faq.md b/ch6-cloud/ch6-11-faq.md index 4ec1bfe..3d7700f 100644 --- a/ch6-cloud/ch6-11-faq.md +++ b/ch6-cloud/ch6-11-faq.md @@ -1,4 +1,4 @@ -# 6.12. 补充说明 +# 6.11. 补充说明 分布式是很大的领域,本章中的介绍只能算是对领域的管中窥豹。因为大型系统流量大,并发高,所以往往很多朴素的方案会变得难以满足需求。人们为了解决大型系统场景中的各种问题,而开发出了各式各样的分布式系统。有些系统非常简单,比如本章中介绍的分布式 id 生成器,而有一些系统则可能非常复杂,比如本章中的分布式搜索引擎(当然,本章中提到的 es 不是 Go 实现)。 diff --git a/ch6-cloud/readme.md b/ch6-cloud/readme.md index 6f43692..dbf0f13 100644 --- a/ch6-cloud/readme.md +++ b/ch6-cloud/readme.md @@ -1,4 +1,4 @@ -# 第六章 分布式系统 +# 第6章 分布式系统 Go语言号称是互联网时代的C语言。现在的互联网系统已经不是以前的一个主机搞定一切的时代,互联网时代的服务后台有大量的分布式系统构成,任何单一后台服务器节点的故障并不会导致整个系统的停机。同时以青云、阿里云、腾讯云为代表的云厂商崛起标志着云时代的到来,在云时代分布式编程将成为一个基本技能。而基于Go语言构建的Docker、K8s等系统正是推动了云时代的提前到来。