diff --git a/SUMMARY.md b/SUMMARY.md index f3e15da..e2e1ac5 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -26,29 +26,43 @@ * [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. 函数(Doing)](ch3-asm/ch3-04-func.md) - * [3.5. 控制流(TODO)](ch3-asm/ch3-05-control-flow.md) - * [3.6. 再论函数(TODO)](ch3-asm/ch3-06-func-again.md) - * [3.7. FUNCDATA和PCDATA(TODO)](ch3-asm/ch3-07-funcdata-pcdata.md) - * [3.8. C预处理器(TODO)](ch3-asm/ch3-08-c-preprocessors.md) - * [3.9. Go核心对象结构(TODO)](ch3-asm/ch3-09-core-type.md) - * [3.10. runtime内置函数(TODO)](ch3-asm/ch3-10-runtime-func.md) - * [3.11. 调用C函数(TODO)](ch3-asm/ch3-11-call-c-leaf-func.md) - * [3.12. AVX/SSE/JIT高级优化(TODO)](ch3-asm/ch3-12-avx-sse-jit.md) - * [3.13. ARM汇编(TODO)](ch3-asm/ch3-13-arm.md) - * [3.14. 补充说明(TODO)](ch3-asm/ch3-14-faq.md) -* [第四章 移动平台(TODO)](ch4-mobile/readme.md) -* [第五章 这是一个坑(TODO)](ch5-wtf/readme.md) -* [第六章 Go和Web](ch6-web/readme.md) - * [6.1. Web开发简介](ch6-web/ch6-01-introduction.md) - * [6.2. Router请求路由](ch6-web/ch6-02-router.md) - * [6.3. Middleware中间件](ch6-web/ch6-03-middleware.md) - * [6.4. Validator请求校验](ch6-web/ch6-04-validator.md) - * [6.5. Database和数据库打交道](ch6-web/ch6-05-database.md) - * [6.8. Layout大型web项目分层](ch6-web/ch6-08-layout-of-web-project.md) - * [6.12. Load-balance负载均衡](ch6-web/ch6-12-load-balance.md) -* [第七章 Go和那些生产力工具](ch7-tools/readme.md) - * [7.1. json2go](ch7-tools/ch7-01-json2go.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-faq.md) +* [第四章 RPC和Protobuf](ch4-rpc/readme.md) + * [4.1. RPC入门(TODO)](ch4-rpc/ch4-01-rpc-intro.md) + * [4.2. Protobuf简介(TODO)](ch4-rpc/ch4-02-pb-intro.md) + * [4.3. protorpc(TODO)](ch4-rpc/ch4-03-protorpc.md) + * [4.4. grpc(TODO)](ch4-rpc/ch4-04-grpc.md) + * [4.5. 反向rpc(TODO)](ch4-rpc/ch4-05-reverse-rpc.md) + * [4.6. Protobuf扩展(TODO)](ch4-rpc/ch4-06-pb-option.md) + * [4.7. 基于pb的rpc定制(TODO)](ch4-rpc/ch4-07-pb-rpc.md) + * [4.8. 补充说明(TODO)](ch4-rpc/ch4-08-faq.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.7. Layout大型web项目分层](ch5-web/ch5-07-layout-of-web-project.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. Raft协议(TODO)](ch6-cloud/ch6-02-raft.md) + * [6.3. 分布式哈希(TODO)](ch6-cloud/ch6-03-hash.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. confd(TODO)](ch6-cloud/ch6-07-confd.md) + * [6.8. 分布式锁(TODO)](ch6-cloud/ch6-08-lock.md) + * [6.9. 分布式任务调度系统(TODO)](ch6-cloud/ch6-09-sched.md) + * [6.10. 延时任务系统(TODO)](ch6-cloud/ch6-10-delay-job.md) + * [6.11. Kubernetes(TODO)](ch6-cloud/ch6-11-k8s.md) + * [6.12. 补充说明(TODO)](ch6-cloud/ch6-12-faq.md) +* [第七章 Go和AST](ch7-ast/readme.md) +* [第八章 Go和那些生产力工具](ch8-tools/readme.md) * [附录](appendix/readme.md) * [附录A: Go语言常见坑](appendix/appendix-a-trap.md) * [附录B: 参考资料](appendix/appendix-b-ref.md) diff --git a/appendix/appendix-c-author.md b/appendix/appendix-c-author.md index 827ef4b..76daed6 100644 --- a/appendix/appendix-c-author.md +++ b/appendix/appendix-c-author.md @@ -1,3 +1,4 @@ # 附录C:作者简介 -- [柴树杉(网络ID:chai2010)](https://github.com/chai2010) 国内第一批Go语言爱好者,创建了最早的QQ讨论组和golang-china邮件列表,组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作,Go语言代码的贡献者。 +- [柴树杉(网络ID:chai2010)](https://github.com/chai2010) 国内第一批Go语言爱好者,创建了最早的QQ讨论组和golang-china邮件列表,组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作,Go语言代码的贡献者。目前在[青云QingCloud](https://www.qingcloud.com/)从事开源的多云应用管理平台[OpenPitrix](https://github.com/openpitrix/openpitrix)开发工作。 +- [cch123](https://github.com/cch123): TODO diff --git a/ch1-basic/ch1-03-array-string-and-slice.md b/ch1-basic/ch1-03-array-string-and-slice.md index d1fdc8b..1742dfe 100644 --- a/ch1-basic/ch1-03-array-string-and-slice.md +++ b/ch1-basic/ch1-03-array-string-and-slice.md @@ -268,7 +268,7 @@ fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界 ```go func forOnString(s string, forBody func(i int, r rune)) { for i := 0; len(s) > 0; { - r, size := utf8.DecodeRuneInString(str) + r, size := utf8.DecodeRuneInString(s) forBody(i, r) s = s[size:] i += size diff --git a/ch1-basic/ch1-05-mem.md b/ch1-basic/ch1-05-mem.md index b55ac10..7921a3d 100644 --- a/ch1-basic/ch1-05-mem.md +++ b/ch1-basic/ch1-05-mem.md @@ -43,6 +43,8 @@ func worker(wg *sync.WaitGroup) { } func main() { + var wg sync.WaitGroup + wg.Add(2) go worker(&wg) go worker(&wg) wg.Wait() @@ -84,7 +86,7 @@ func main() { `atomic.AddUint64`函数调用保证了`total`的读取、更新和保存是一个原子操作,因此在多线程中访问也是安全的。 -原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态通过降低互斥锁的次数来提高性能。 +原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。 ```go type singleton struct {} diff --git a/ch3-asm/ch3-04-func.md b/ch3-asm/ch3-04-func.md index 584cbec..4d09c52 100644 --- a/ch3-asm/ch3-04-func.md +++ b/ch3-asm/ch3-04-func.md @@ -1,5 +1,250 @@ -# 3.4. 函数(Doing) +# 3.4. 函数 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了。只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 -TODO +## 基本语法 + +函数标识符通过TEXT汇编指令定义,表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现,但是对于TEXT指令本身来说并不关心后面是否有指令。我个人绝对TEXT和LABEL定义的符号是类似的,区别只是LABEL是用于跳转标号,但是本质上他们都是通过标识符映射一个内存地址。 + +函数的定义的语法如下: + +``` +TEXT symbol(SB), [flags,] $framesize[-argsize] +``` + +函数的定义部分由5个部分组成:TEXT指令、函数名、可选的flags标志、函数帧大小和可选的函数参数大小。 + +其中Text用于定义函数符号,函数名中当前包的路径可以省略。函数的名字后面是`(SB)`,表示是相对于的函数名符号对相对于SB伪寄存器的偏移量,二者组合在一起最终是绝对地址。作为全局的标识符的全局变量和全局函数的名字一般都是基于SB伪寄存器的相对地址。标志部分用于指示函数的一些特殊行为,常见的NOSPLIT主要用于指示叶子函数不进行栈分裂。framesize部分表示函数的局部变量需要多少栈空间,其中包含调用其它函数是准备调用参数的隐式栈空间。最后是可以省略的参数大小,之所以可以省略是因为编译器可以从Go语言的函数声明中推导出函数参数的大小。 + +下面是在main包中Add在汇编中两种定义方式: + +``` +// func Add(a, b int) int +TEXT main·Add(SB), NOSPLIT, $0-24 + +// func Add(a, b int) int +TEXT ·Add(SB), $0 +``` + +第一种是最完整的写法:函数名部分包含了当前包的路径,同时指明了函数的参数大小为24个字节(对应参数和返回值的3个int类型)。第二种写法则比较简洁,省略了当前包的路径和参数的大小。需要注意的是,标志参数中的NOSPLIT如果在Go语言函数声明中通过注释指明了标志,应该也是可以省略的(需要确认下)。 + +目前可能遇到的函数函数标志有NOSPLIT、WRAPPER和NEEDCTXT几个。其中NOSPLIT不会生成或包含栈分裂代码,这一般用于没有任何其它函数调用的叶子函数,这样可以适当提高性能。WRAPPER标志则表示这个是一个包装函数,在panic或runtime.caller等某项处理函数帧的地方不会增加函数帧计数。最后的NEEDCTXT表示需要一个上下午参数,一般用于闭包函数。 + +需要注意的是函数也没有类型,上面定义的Add函数签名可以下面任意一种格式: + +``` +func Add(a, b int) int +func Add(a, b, c int) +func Add() (a, b, c int) +func Add() (a []int) // reflect.SliceHeader 切片头刚好也是 3 个 int 成员 +// ... +``` + +对于汇编函数来说,只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中,输入参数和返回值参数是没有任何的区别的。 + +## 函数参数和返回值 + +对于函数来说,最重要是是函数对外提供的API约定,包含函数的名称、参数和返回值。当名称和参数返回都确定之后,如何精确计算参数和返回值的大小是第一个需要解决的问题。 + +比如有一个Foo函数的签名如下: + +```go +func Foo(a, b int) (c int) +``` + +对于这个函数,我们可以轻易看出它需要3个int类型的空间,参数和返回值的大小也就是24个字节: + +``` +TEXT ·Foo(SB), $0-24 +``` + +那么如何在汇编中引用这3个参数呢?为此Go汇编中引入了一个FP伪寄存器,表示函数当前帧的地址,也就是第一个参数的地址。因此我们以通过`+0(FP)`、`+8(FP)`和`+16(FP)`来分别引用a、b、c三个参数。 + +但是在汇编代码中,我们并不能直接使用`+0(FP)`来使用参数。为了编写易于维护的汇编代码,Go汇编语言要求,任何通过FP寄存器访问的变量必和一个临时标识符前缀组合后才能有效,一般使用参数对应的变量名作为前缀。 + +下面的代码演示了如何在汇编函数中使用参数和返回值: + +``` +TEXT ·Foo(SB), $0 + MOVEQ a+0(FP), AX // a + MOVEQ b+8(FP), BX // b + MOVEQ c+16(FP), CX // c + RET +``` + +如果是参数和返回值类型比较复杂的情况改如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数: + +```go +func SomeFunc(a, b int, c bool) (d float64, err error) int +``` + +函数的参数有不同的类型,同时含义多个返回值,而且返回值中含有更复杂的接口类型。我们该如何计算每个参数的位置和总的大小呢? + +其实函数参数和返回值的大小以及对齐问题和结构体的大小和成员对齐问题是一致的。我们先看看如果用Go语言函数来模拟Foo函数中参数和返回值的地址: + +```go +func Foo(FP *struct{a, b, c int}) { + _ = unsafe.Offsetof(FP.a) + uintptr(FP) // a + _ = unsafe.Offsetof(FP.b) + uintptr(FP) // b + _ = unsafe.Offsetof(FP.c) + uintptr(FP) // c + + _ = unsafe.Sizeof(*FP) // argsize + + return +} +``` + +我们尝试将全部的参数和返回值以同样的顺序放到一个结构体中,将FP伪寄存器作为唯一的一个指针参数,而每个成员的地址也就是对应原来参数的地址。 + +用同样的策略可以很容易计算前面的SomeFunc函数的参数和返回值的地址和总大小。 + +因为SomeFunc函数的参数比较多,我们临时定一个`SomeFunc_args_and_returns`结构体用于对应参数和返回值: + +```go +type SomeFunc_args_and_returns struct { + a int + b int + c bool + d float64 + e error +} +``` + +然后将SomeFunc原来的参数替换为结构体形式,并且只保留唯一的FP作为参数: + +```go +func SomeFunc(FP *SomeFunc_args_and_returns) { + _ = unsafe.Offsetof(FP.a) + uintptr(FP) // a + _ = unsafe.Offsetof(FP.b) + uintptr(FP) // b + _ = unsafe.Offsetof(FP.c) + uintptr(FP) // c + _ = unsafe.Offsetof(FP.d) + uintptr(FP) // d + _ = unsafe.Offsetof(FP.e) + uintptr(FP) // e + + _ = unsafe.Sizeof(*FP) // argsize + + return +} +``` + +代码完全和Foo函数参数的方式类似。唯一的差异是每个函数的偏移量,这有`unsafe.Offsetof`函数自动计算生成。因为Go结构体中的每个成员已经满足了对齐要求,因此采用通用方式得到每个参数的偏移量也是满足对齐要求的。 + + +## 函数中的局部变量 + +从Go语言函数角度讲,局部变量是函数内明确定义的变量,同时也包含函数的参数和返回值变量。但是从Go汇编角度看,局部变量是指函数运行时,在当前函数栈帧所对应的内存内的变量,不包含函数的参数和返回值(因为访问方式有差异)。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解,我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量,不包含参数和返回值部分。 + +为了便于访问局部变量,Go汇编语言引入了伪SP寄存器,对应当前栈帧的底部。因为在当前栈帧时间栈的底部是固定不变的,因此局部变量的相对于伪SP的偏移量也就是固定的,这可以简化局部变量的维护工作。SP真伪区分只有一个原则:如果使用SP时有一个临时标识符前缀就是伪SP,否则就是真SP寄存器。比如`a(SP)`和`b+8(SP)`有a和b临时前缀,这里都是伪SP,而前缀部分一般用于表示局部变量的名字。而`(SP)`和`+8(SP)`没有临时标识符作为前缀,它们都是真SP寄存器。 + +在X86平台,函数的调用栈是从高地址向低地址增长的,因此伪SP寄存器对应栈帧的底部其实是对应更大的地址。当前栈的顶部对应真实存在的SP寄存器,对应对应当前函数栈帧的栈底,对应更小的地址。如果整个内容是用Memory数组表示,那么`Memory[0(SP):end-0(SP)]`就是对应当前栈帧的切片,其中开始位置是真SP,结尾部分是伪SP。真SP一般用于表示调用其它函数时的参数和返回值,真SP对应内存较低的地址,所以被访问变量的偏移量是正数;而伪SP对应高地址,对应的局部变量的偏移量都是负数。 + +我们现在Go语言定义一个Foo函数,并在函数内部定义几个局部变量: + +```go +func Foo() { var a, b, c int } +``` + +然后通过汇编语言重新实现Foo函数,并通过伪SP来定位局部变量: + +``` +TEXT ·Foo(SB), $24-0 + MOVQ a-8*3(SP), AX // a + MOVQ b-8*2(SP), BX // b + MOVQ c-8*1(SP), CX // c + RET +``` + +Foo函数有3个int类型的局部变量,但是没有调用其它的函数,所以函数的栈帧大小为24个字节。因为Foo函数没有参数和返回值,因此参数和返回值大小为0个字节,当然这个部分可以省略不写。而局部变量中先定义的变量a离为SP对应的地址最远,最后定义的变量c里伪SP最近。有两个隐私导致出现这种逆序的结果:一个从Go语言函数角度理解,先定义的a变量地址要比后定义的变量的地址更小;另一个是伪SP对应栈帧的底部,而栈是从高向地生长的,所以有着更小地址的a变量离栈的底部伪SP更远。 + +我们同样可以通过结构体来模拟局部变量的布局: + +```go +func Foo() { + var local [1]struct{a, b, c int}; + var SP = &local[1]; + + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.a)) + uintptr(&SP) // a + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.b)) + uintptr(&SP) // b + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.c)) + uintptr(&SP) // c +} +``` + +我们将之前的三个局部变量挪到一个结构体中。然后构造一个SP变量对应伪SP寄存器,对应局部变量结构体的顶部。然后根据局部变量总大小和每个变量对应成员的偏移量计算相对于伪SP的距离,最终偏移量是一个负数。 + +通过这种方式可以处理复制的局部变量的偏移,同时也能包装每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据字节的习惯组织变量的布局。 + + +## 调用其它函数 + +常见的用Go汇编实现的函数都是叶子函数,也就是被其它函数调用,但是很少调用其它函数。这主要是因为叶子函数比较简单,可以简化汇编函数的编写;同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用通用重要,否则Go汇编就不是一个完整的汇编语言。 + +在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是有调用方准备的:调用方在栈上设置好空间和数据后调用函数,被调用方在返回前将返回值放如对应的位置,函数通过RET指令返回调用放函数之后,调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的,这样做的有点是函数调用栈比较清晰,缺点是函数调用有一定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响)。 + +为了便于演示,我们先用Go语言构造foo和bar两个函数,其中foo函数内部调用bar函数: + +```go +func foo() { + var a, b int + bar(b) +} + +func bar(a int) int { + return a +} +``` + +然后用汇编重新实现类似的函数: + +``` +TEXT ·foo(SB), $32-0 + MOVQ a-8*2(SP), AX // a + MOVQ b-8*1(SP), BX // b + + MOVQ BX, +0(SP) // bar(BX) + CALL ·bar(SB) // + MOVQ +8(SP), CX // CX = bar(a) + RET + +TEXT ·bar(SB), $0-16 + MOVQ a-0(FP), AX // a + + MOVQ AX, ret+8(FP) // return a + RET +``` + +首选分享foo函数的栈帧的大小:foo函数内部有a、b两个局部变量占用16个字节,然后要需给要调用的bar函数准备的参数和返回值准备16字节的空间,因此总共有32字节的栈帧大小。在调用bar函数前我们已经计算好了栈帧的大小,Go汇编语言环境已经真实的SP寄存器调整到合适的大小,在调用函数时刻并不需要再手动调整SP寄存器。在调用函数bar前,真SP对应向下增长的栈顶部,因此顶部的16个字节和bar函数的参数和返回值是对应的相同的内存空间。我们将保存了b只的BX寄存器内容放入`+0(SP)`位置,也就是准备bar函数的第一个参数。然后通过CALL指令进行函数调用。在bar函数内,首先从第一个参数对应的`+0(FP)`位置去除参数值存入AX寄存器,然后再将AX内容放入返回值对应的`ret+8(FP)`内存位置,最后调用RET返回。在foo函数中,调用bar函数返回后,从bar函数返回值对应的`+8(SP)`位置取出结果放到CX寄存,从而完成函数调用。 + +调用其它函数前调用方要选择保存相关寄存器到栈中,并在调用函数返回后选择要恢复的寄存器进行保存。Go语言中函数调用时一个复杂的问题,因为Go函数不仅仅要了解函数调用函数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。 + +## 宏函数 + +宏函数并不是Go汇编语言所定义,二是Go汇编引入的预处理特性自带的特性。 + +在C语言中我们可以通过带参数的宏定义一个交换2个数的宏函数: + +```c +#define SWAP(x, y) do{ int t = x; x = y; y = t; }while(0) +``` + +我们可以用类似的方式定义一个交换两个寄存器的宏: + +```c +#define SWAP(x, y, t) MOVQ x, t; MOVQ y, x; MOVQ t, y +``` + +因为汇编语言中无法定义临时变量,我们增加一个参数用于临时寄存器。下面是通过SWAP宏函数交换AX和BX寄存器的值,然后返回结果: + +``` +// func Swap(a, b int) (int, int) +TEXT ·Swap(SB), $0-32 + MOVQ a-8*2(SP), AX // a + MOVQ b-8*1(SP), BX // b + + SWAP(AX, BX, CX) // AX, BX = b, a + + MOVQ AX, ret0+16(FP) // return + MOVQ BX, ret1+24(FP) // + RET +``` + +因为预处理器可以通过条件编译针对不同的平台定义宏的实现,这样可以简化平台带来的差异。 + diff --git a/ch3-asm/ch3-05-control-flow.md b/ch3-asm/ch3-05-control-flow.md index f0e8d3b..8b38c60 100644 --- a/ch3-asm/ch3-05-control-flow.md +++ b/ch3-asm/ch3-05-control-flow.md @@ -1,3 +1,241 @@ -# 3.5. 控制流(TODO) +# 3.5. 控制流 -TODO +程序执行的流程主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码。 + +## 顺序执行 + +顺序执行是我们比较熟悉的工作模式,类似俗称流水账编程。所有不含分支、循环和goto语言,并且每一递归调用的Go函数一般都是顺序执行的。 + +比如有如下顺序执行的代码: + +```go +func main() { + var a = 10 + println(a) + + var b = (a+a)*a + println(b) +} +``` + +我们尝试用Go汇编的思维改写上述函数。因为X86指令中一般只有2个操作数,因此在用汇编改写时要求出现的变量表达式中最多只能有一个运算符。同时对于一些函数调用,也需要该用汇编中可以调用的函数来改写。 + +第一步改写依然是使用Go语言,只不过是用汇编的思维改写: + +``` +func main() { + var a, b int + + a = 10 + runtime.printint(a) + runtime.printnl() + + b = a + b += b + b *= a + runtime.printint(b) + runtime.printnl() +} +``` + +首选模仿C语言的处理方式在函数入口出声明全部的局部变量。然后将根据MOV、ADD、MUL等指令的风格,将之前的变量表达式展开为用`=`、`+=`和`*=`几种运算表达的多个指令。最后用runtime包内部的printint和printnl函数代替之前的println函数输出结果。 + +经过用汇编的思维改写过后,上述的Go函数虽然看着繁琐了一点,但是还是比较容易理解的。下面我们进一步尝试将改写后的函数继续转译为汇编函数: + +``` +TEXT ·main(SB), $24-0 + MOVQ $0, a-8*2(SP) // a = 0 + MOVQ $0, b-8*1(SP) // b = 0 + + // 将新的值写入a对应内存 + MOVQ $10, AX // AX = 10 + MOVQ AX, a-8*2(SP) // a = AX + + // 以a为参数调用函数 + MOVQ AX, 0(SP) + CALL runtime·printint + CALL runtime·printnl + + // 函数调用后, AX/BX 可能被污染, 需要重新加载 + MOVQ a-8*2(SP), AX // AX = a + MOVQ b-8*1(SP), BX // BX = b + + // 计算b值, 并写入内存 + MOVQ AX, BX // BX = AX // b = a + ADDQ BX, BX // BX += BX // b += a + MULQ AX, BX // BX *= AX // b *= a + MOVQ BX, b-8*1(SP) // b = BX + + // 以b为参数调用函数 + MOVQ BX, 0(SP) + CALL runtime·printint + CALL runtime·printnl + + RET +``` + +汇编实现main函数的第一步是要计算函数栈帧的大小。因为函数内有a、b两个int类型变量,同时调用的runtime·printint函数参数是一个int类型并且没有返回值,因此main函数的栈帧是3个int类型组成的24个字节的栈内存空间。 + +在函数的开始处先将变量初始化为0值,其中`a-8*2(SP)`对应a变量、`a-8*1(SP)`对应b变量(因为a变量先定义,因此a变量的地址更小)。 + +然后给a变量分配一个AX寄存器,并且通过AX寄存器将a变量对应的内存设置为10,AX也是10。为了输出a变量,需要将AX寄存器的值放到`0(SP)`位置,这个位置的变量将在调用runtime·printint函数时作为它的参数被打印。因为我们之前已经将AX的值保存到a变量内存中了,因此在调用函数前并不需要在进行寄存器的备份工作。 + +在调用函数返回之后,全部的寄存器将被视为被调用的函数修改,因此我们需要从a、b对应的内存中重新恢复寄存器AX和BX。然后参考上面Go语言中b变量的计算方式更新BX对应的值,计算完成后同样将BX的值写入到b对应的内存。 + +最后以b变量作为参数再次调用runtime·printint函数进行输出工作。所有的寄存器通样可能被污染,不过main马上就返回不在需要使用AX、BX等寄存器,因此就不需要再次恢复寄存器的值了。 + +重新分析汇编改写后的整个函数会发现里面很多的冗余代码。我们并不需要a、b两个临时变量分配两个内存空间,而且也不需要在每个寄存器变化之后都要写入内存。下面是经过优化的汇编函数: + +``` +TEXT ·main(SB), $16-0 + // var temp int + + // 将新的值写入a对应内存 + MOVQ $10, AX // AX = 10 + MOVQ AX, temp-8(SP) // temp = AX + + // 以a为参数调用函数 + CALL runtime·printint + CALL runtime·printnl + + // 函数调用后, AX 可能被污染, 需要重新加载 + MOVQ temp-8*1(SP), AX // AX = temp + + // 计算b值, 不需要写入内存 + MOVQ AX, BX // BX = AX // b = a + ADDQ BX, BX // BX += BX // b += a + MULQ AX, BX // BX *= AX // b *= a + + // ... +``` + +首先是将main函数的栈帧大小从24字节减少到16字节。唯一需要保存的是a变量的值,因此在调用runtime·printint函数输出时全部的寄存器都可能被污染,我们无法通过寄存器备份a变量的值,只有在栈内存中的值才是安全的。然后在BX寄存器并不需要保存到内存。其它部分的代码基本保持不变。 + +## if/goto跳转 + +早期的Go虽然提供了goto语句,但是并不推荐在编程中使用。有一个和cgo类似的原则:如果可以不使用goto语句,那么就不要使用goto语句。Go语言中的goto语句是有严格限制的:它无法跨越代码块,并且在被跨越的代码中不能含义变量定义的语句。虽然Go语言不喜欢goto,但是goto确实每个汇编语言码农的最爱。goto近似等价于汇编语言中的无条件跳转指令JMP,配合if条件goto就组成了有条件跳转指令,而有条件跳转指令正是构建整个汇编代码控制流的基石。 + +为了便于理解,我们用Go语言构造一个模拟三元表达式的If函数: + +```go +func If(ok bool, a, b int) int { + if ok { return a } else { return b } +} +``` + +比如求两个数最大值的三元表达式`(a>b)?a:b`用If函数可以这样表达:`If(a>b, a, b)`。因为语言的限制,用来模拟三元表达式的If函数不支持范型(可以将a、b和返回类型改为空接口,使用会繁琐一些)。 + +这个函数虽然看似只有简单的一行,但是包含了if分支语句。在改用汇编实现前,我们还是先用汇编的思维来重写If函数。在改写时同样要遵循每个表达式只能有一个运算符的限制,同时if语句的条件部分必须只有一个比较符号组成,if语句的body部分只能是一个goto语句。 + +用汇编思维改写后的If函数实现如下: + +```go +func If(ok int, a, b int) int { + if ok == 0 { goto L } + return a +L: + return b +} +``` + +因为汇编语言中没有bool类型,我们改用int类型代替bool类型(真实的汇编是用byte表示bool类型,可以通过MOVBQZX指令加载byte类型的值)。当ok参数非0时返回变量a,否则返回变量b。我们将ok的逻辑反转下:当ok参数为0时,表示返回b,否则返回变量a。在if语句中,当ok参数为0时goto到L标号指定的语句,也就是返回变量b。如果if条件不满足,也就考试ok残非0,执行后门的语句返回变量a。 + +上述函数的实现已经非常接近汇编语言,下面是改为汇编实现的代码: + +``` +TEXT ·If(SB), NOSPLIT, $0-32 + MOVQ ok+8*0(FP), CX // ok + MOVQ a+8*1(FP), AX // a + MOVQ b+8*2(FP), BX // b + + CMPQ CX, $0 // test ok + JZ L // if ok == 0, skip 2 line + MOVQ AX, ret+24(FP) // return a + RET + +L: + MOVQ BX, ret+24(FP) // return b + RET +``` + +首选是将三个参数加载到寄存器中,ok参数对应CX寄存器,a、b分别对应AX、BX寄存器。然后使用CMPQ比较指令将CX寄存器和常数0进行比较。如果比较的结果为0,那么下一条JZ为0时跳转指令将跳转到L标号对应的指令,也就是返回变量b的值。如果比较的结果不为0,那么JZ指令讲没有效果,继续执行后的指令,也就是返回变量a的值。 + +在跳转指令中,跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中,更希望通过相对位置跳转,这时候可以通过PC寄存器的来计算跳转的位置。 + +## for循环 + +Go语言的for循环有多种用法,我们这里只选择最经典的for结构来讨论。经典的for循环由初始化、结束条件、迭代步长三个部分组成,再配合循环体内部的if条件语言,这种for结构可以模拟其它各种循环类型。 + +基于经典的for循环结构,我们定一个一个LoopAdd函数,可以用于计算任意等差数列的和: + +```go +func LoopAdd(cnt, v0, step int) int { + result := v0 + for i := 0; i < cnt; i++ { + result += step + } + return result +} +``` + +比如`1+2+...+100`可以这样计算`LoopAdd(100, 1, 1)`,`10+8+...+0`可以这样计算`LoopAdd(5, 10, -2)`。现在采用前面`if/goto`类似的技术来改造for循环。 + +新的LoopAdd函数只有if/goto语句构成: + +```go + +func LoopAdd(cnt, v0, step int) int { + var i = 0 + var result = 0 + +LOOP_BEGIN: + result = v0 + +LOOP_IF: + if i < cnt { goto LOOP_BODY } + goto LOOP_END + +LOOP_BODY + i = i+1 + result = result + step + goto LOOP_IF + +LOOP_END: + + return result +} +``` + +函数的开头先定义两个局部变量便于后续代码使用。然后将for语句的初始化、结束条件、迭代步长三个部分拆分为三个代码段,分别用LOOP_BEGIN、LOOP_IF、LOOP_BODY三个标号表示。其中LOOP_BEGIN循环初始化部分只会执行一次,因此该标号并不会被引用,可以省略。最后LOOP_END语句表示for循环的结束。四个标号分隔出的三个代码段分别对应for循环的初始化语句、循环条件和循环体,其中迭代语句被合并到循环体中了。 + +下面用汇编语言重新实现LoopAdd函数 + +``` +// func LoopAdd(cnt, v0, step int) int +TEXT ·LoopAdd(SB), NOSPLIT, $0-32 + MOVQ cnt+0(FP), AX // cnt + MOVQ v0+8(FP), BX // v0/result + MOVQ step+16(FP), CX // step + +LOOP_BEGIN: + MOVQ $0, DX // i + +LOOP_IF: + CMPQ DX, AX // compare i, cnt + JL LOOP_BODY // if i < cnt: goto LOOP_BODY + goto LOOP_END + +LOOP_BODY: + ADDQ $1, DX // i++ + ADDQ CX, BX // result += step + goto LOOP_IF + +LOOP_END: + + MOVQ BX, ret+24(FP) // return result + RET +``` + +其中v0和result变量复用了一个BX寄存器。在LOOP_BEGIN标号对应的指令部分,用MOVQ将DX寄存器初始化为0,DX对应变量i,循环的迭代变量。在LOOP_IF标号对应的指令部分,使用CMPQ指令比较AX和AX,如果循环没有结束则跳转到LOOP_BODY部分,否则跳转到LOOP_END部分结束循环。在LOOP_BODY部分,更新迭代变量并且执行循环体中到累加语句,然后直接跳转到LOOP_IF部分进入下一轮循环条件判断。LOOP_END标号之后就是返回返回累加结果到语句。 + +循环是最复杂到控制流,循环中隐含了分支和跳转语句。掌握了循环到下方基本也就掌握了汇编语言到写法。掌握规律之后,其实汇编语言编程会变得异常简单。 diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index be92c39..e331323 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -1,3 +1,239 @@ -# 3.6. 再论函数(TODO) +# 3.6. 再论函数 + +在前面的章节中我们已经简单讨论过Go的汇编函数,但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数,也就是栈的大小是可以预期的,叶子函数也就是可以基本忽略爆栈的问题(如果已经爆了,那也是上级函数的问题)。如果没有爆栈问题,那么也就是不会有栈的分裂问题;如果没有栈的分裂也就不需要移动栈上的指针,也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的,永远不用担心爆栈的风险。那么这些近似黑科技的特殊是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。 + +## 递归函数: 1到n求和 + +递归函数是比较特殊的函数,递归函数通过调用自身并且在栈上保存状态,这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题,因为栈可以根据需要进行扩容和收缩。我们现在尝试通过汇编语言实现一个递归调用的函数,为了简化目前先不考虑栈的变化。 + +先通过Go递归函数实现一个1到n的求和函数: + +```go +// sum = 1+2+...+n +// sum(100) = 5050 +func sum(n int) int { + if n > 0 { return n+sum(n-1) } else { return 0 } +} +``` + +然后通过if/goto构型重新上面的递归函数,以便于转义为汇编版本: + +```go +func sum(n int) (result int) { + var AX = n + var BX int + + if n > 0 { goto L_STEP_TO_END } + goto L_END + +L_STEP_TO_END: + AX -= 1 + BX = sum(AX) + + AX = n // 调用函数后, AX重新恢复为n + BX += AX + + return BX + +L_END: + return 0 +} +``` + +在改写之后,递归调用的参数需要引入局部变量,保存中间结果也需要引入局部变量。而通过栈来保存中间的调用状态正是递归函数的核心。因为输入参数也在栈上,因为我们可以通过输入参数来保存少量的状态。同时我们模拟定义了AX和BX寄存器,寄存器在使用前需要初始化,并且在函数调用后也需要重新初始化。 + +下面继续改造为汇编语言版本: + +``` +// func sum(n int) (result int) +TEXT ·sum(SB), NOSPLIT, $16-16 + MOVQ n+0(FP), AX // n + MOVQ result+8(FP), BX // result + + CMPQ AX, $0 // test n - 0 + JG L_STEP_TO_END // if > 0: goto L_STEP_TO_END + JMP L_END // goto L_STEP_TO_END + +L_STEP_TO_END: + SUBQ $1, AX // AX -= 1 + MOVQ AX, 0(SP) // arg: n-1 + CALL ·sum(SB) // call sum(n-1) + MOVQ 8(SP), BX // BX = sum(n-1) + + MOVQ n+0(FP), AX // AX = n + ADDQ AX, BX // BX += AX + MOVQ BX, result+8(FP) // return BX + RET + +L_END: + MOVQ $0, result+8(FP) // return 0 + RET +``` + +在汇编版本函数中并没有定义局部变量,只有用于调用自身的临时栈空间。因为函数本身的参数和返回值有16个字节,因此栈帧的大小也为16字节。L_STEP_TO_END标号部分用于处理递归调用,是函数比较复杂的部分。L_END用于处理递归终结的部分。 + +调用sum函数的参数在`0(SP)`位置,调用结束后的返回值在`8(SP)`位置。在函数调用之后要需要重新为需要的寄存器注入值,因为被调用的函数内部很可能会破坏了寄存器的状态。同时调用函数的参数值也可信任的,输入参数也可能在被调用函数内部被修改了值。 + +总得来说用汇编实现递归函数和普通函数并没有什么区别,当然是在没有考虑爆栈的前提下。我们的函数应该可以对较小的n进行求和,但是当n大到一定层度,也就是栈达到一定的深度,必然会出现爆栈的问题。爆栈是C语言的特性,不应该在哪怕是Go汇编语言中出现。 + +## 栈的扩容和收缩 + +Go语言的编译器在生成函数的机器代码时,会在开头插入以小段代码。插入的代码可以做很多事情,包括触发runtime.Gosched进行协作式调度,还包括栈的动态增长等。其实栈等扩容工作主要在runtime包的runtime·morestack_noctxt函数实现,这是一个底层函数,只有汇编层面才可以调用。 + +在新版本的sum汇编函数中,我们在开头和末尾都引入了部分代码: + +``` +// func sum(n int) int +TEXT ·sum(SB), $16-16 + NO_LOCAL_POINTERS + +L_START: + MOVQ TLS, CX + MOVQ 0(CX)(TLS*1), AX + CMPQ SP, 16(AX) + JLS L_MORE_STK + + // 原来的代码 + +L_MORE_STK: + CALL runtime·morestack_noctxt(SB) + JMP L_START +``` + +其中NO_LOCAL_POINTERS表示没有局部指针。因为新引入的代码可能导致调用runtime·morestack_noctxt函数,而栈的扩容必然要涉及函数参数和局部编指针的调整,如果缺少局部指针信息将导致扩容工作无法进行。不仅仅是栈的扩容需要函数的参数和局部指针标记表格,在GC进行垃圾回收时也将需要。函数的参数和返回值的指针状态可以通过在Go语言中的函数声明中获取,函数的局部变量则需要手工指定。因为手工指定指针表格是一个非常繁琐的工作,因此一般要避免在手写汇编中出现局部指针。 + +喜欢深究的读者可能会有一个问题:如果进行垃圾回收或栈调整时,寄存器中的指针时如何维护的?前文说过,Go语言的函数调用时通过栈进行传递参数的,并没有使用寄存器传递参数。同时函数调用之后所有的寄存器视为失效。因此在调整和维护指针时,只需要扫描内存中的指针数据,寄存器中的数据在垃圾回收器函数返回后都需要重新加载,因此寄存器是不需要扫描的。 + +在Go语言的Goroutine实现中,每个TlS线程局部变量会保存当前Goroutine的信息结构体的指针。通过`MOVQ TLS, CX`和`MOVQ 0(CX)(TLS*1), AX`两条指令将表示当前Goroutine信息的g结构体加载到CX寄存器。g结构体在`$GOROOT/src/runtime/runtime2.go`文件定义,开头的结构成员如下: + +```go +type g struct { + // Stack parameters. + // stack describes the actual stack memory: [stack.lo, stack.hi). + // stackguard0 is the stack pointer compared in the Go stack growth prologue. + // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. + // stackguard1 is the stack pointer compared in the C stack growth prologue. + // It is stack.lo+StackGuard on g0 and gsignal stacks. + // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). + stack stack // offset known to runtime/cgo + stackguard0 uintptr // offset known to liblink + stackguard1 uintptr // offset known to liblink + + ... +} +``` + +第一个成员是stack类型,表示当前栈的开始和结束地址。stack的定义如下: + +```go +// Stack describes a Go execution stack. +// The bounds of the stack are exactly [lo, hi), +// with no implicit data structures on either side. +type stack struct { + lo uintptr + hi uintptr +} +``` + +在g结构体中的stackguard0成员是出现爆栈前的警戒线。stackguard0的偏移量是16个字节,因此上述代码中的`CMPQ SP, 16(AX)`表示将当前的真实SP和爆栈警戒线比较,如果超出警戒线则表示需要进行栈扩容,也就是跳转到L_MORE_STK。在L_MORE_STK标号处,线调用runtime·morestack_noctxt进行栈扩容,然后又跳回到函数到开始位置,此时此刻函数到栈已经调整了。然后再进行一次栈大小到检测,如果依然不足则继续扩容,直到栈足够大为止。 + +以上是栈的扩容,但是栈到收缩是在何时处理到呢?我们知道Go运行时会定期进行垃圾回收操作,这其中栈的回收工作。如果栈使用到比例小于一定到阈值,则分配一个较小到栈空间,然后将栈上面到数据移动到新的栈中,栈移动的过程和栈扩容的过程类似。 + +## PCDATA和FUNCDATA + +Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置,以及函数的调用链。因此在panic异常或用log输出信息时,可以精确定位代码的位置。 + +比如以下代码可以打印程序的启动流程: + +```go +func main() { + for skip := 0; ; skip++ { + pc, file, line, ok := runtime.Caller(skip) + if !ok { + break + } + + p := runtime.FuncForPC(pc) + fnfile, fnline := p.FileLine(0) + + fmt.Printf("skip = %d, pc = 0x%08X\n", skip, pc) + fmt.Printf(" func: file = %s, line = L%03d, name = %s, entry = 0x%08X\n", fnfile, fnline, p.Name(), p.Entry()) + fmt.Printf(" call: file = %s, line = L%03d\n", file, line) + } +} +``` + +其中runtime.Caller先获取当时的PC寄存器值,以及文件和行号。然后根据PC寄存器表示的指令位置,通过runtime.FuncForPC函数获取函数的基本信息。Go语言是如何实现这种特性的呢? + +Go语言作为一终静态编译型语言,在执行时每个函数的地址都是固定的,函数的每条指令也时固定的。如果针对每个函数和函数的每个指令生成一个地址表格(也叫PC表格),那么在运行时我们就可以根据PC寄存器的值轻松查询到指令当时对应的函数和位置信息。而Go语言也时采用类似的策略,只不过地址表格经过裁剪,舍弃了不必要的信息。因为要在运行时获取任意一个地址的位置,必然是要有一个函数调用,因此我们只需要为函数的开始和结束位置,以及每个函数调用位置生成地址表格就可以了。同时地址是有大小顺序的,在排序后可以通过只记录增量来减少数据的大小;在查询时可以通过二分法加快查找的速度。 + +在汇编中有个PCDATA用于生成PC表格,PCDATA的指令用法为:`PCDATA tableid, tableoffset`。PCDATA有个两个参数,第一个参数为表格的类型,第二个是表格的地址。在目前的实现中,有PCDATA_StackMapIndex和PCDATA_InlTreeIndex两种表格类型。两种表格的数据是类似的,应该包含了代码所在的文件路径、行号和函数的信息,只不过PCDATA_InlTreeIndex用于内內联函数的表格。 + +此外对于汇编函数中返回值包含指针的类型,在返回值指针被初始化之后需要执行一个GO_RESULTS_INITIALIZED指令: + +```c +#define GO_RESULTS_INITIALIZED PCDATA $PCDATA_StackMapIndex, $1 +``` + +GO_RESULTS_INITIALIZED记录的也是PC表格的信息,表示PC指针越过某个地址之后返回值才完成被初始化的状态。 + +Go语言二进制文件中除了有PC表格,还有FUNC表格用于记录函数的参数、局部变量的指针信息。FUNCDATA指令和PCDATA的格式类似:`FUNCDATA tableid, tableoffset`,第一个参数为表格的类型,第二个是表格的地址。目前的实现中定义了三种FUNC表格类型:FUNCDATA_ArgsPointerMaps表示函数参数的指针信息表,FUNCDATA_LocalsPointerMaps表示局部指针信息表,FUNCDATA_InlTree表示被内联展开的指针信息表。通过FUNC表格,Go语言的垃圾回收器可以跟踪全部指针的生命周期,同时根据指针指向的地址在是否被移动的栈范围来确定是否要进行指针移动。 + +在前面递归函数的例子中,我们遇到一个NO_LOCAL_POINTERS宏。它的定义如下: + +```c +#define FUNCDATA_ArgsPointerMaps 0 /* garbage collector blocks */ +#define FUNCDATA_LocalsPointerMaps 1 +#define FUNCDATA_InlTree 2 + +#define NO_LOCAL_POINTERS FUNCDATA $FUNCDATA_LocalsPointerMaps, runtime·no_pointers_stackmap(SB) +``` + +因此NO_LOCAL_POINTERS宏表示的是FUNCDATA_LocalsPointerMaps对应的局部指针表格,而runtime·no_pointers_stackmap是一个空的指针表格,也就是表示函数没有指针类型的局部变量。 + +PCDATA和FUNCDATA的数据一般是由编译器自动生成的,手工编写并不现实。如果函数已经有Go语言声明,那么编译器可以自动输出参数和返回值的指针表格。同时所有的函数调用一般是对应CALL指令,编译器也是可以辅助生成PCDATA表格的。编译器唯一无法自动生成是函数局部变量的表格,因此我们一般要在汇编函数的局部变量中谨慎使用指针类型。 + +对于PCDATA和FUNCDATA细节敢兴趣的同学可以尝试从debug/gosym包入手,参考包的实现和测试代码。 + + +## 方法函数 + +Go语言中方法函数和全局函数非常相似,比如有以下的方法: + +```go +package main + +type MyInt int + +func (v MyInt) Twice() int { + return int(v)*2 +} + +func MyInt_Twice(v MyInt) int { + return int(v)*2 +} +``` + +其中MyInt类型的Twice方法和MyInt_Twice函数的类型是完全一样的,只不过Twice在目标文件中被修饰为`main.MyInt.Twice`名称。我们可以用汇编实现该方法函数: + +``` +// func (v MyInt) Twice() int +TEXT ·MyInt·Twice(SB), NOSPLIT, $0-16 + MOVQ a+0(FP), AX // v + MOVQ AX, AX // AX *= 2 + MOVQ AX, ret+8(FP) // return v + RET +``` + +不过这只是最多非指针类型的解释函数。现在增加一个接收参数是指针类型的Ptr方法,指针返回传入的指针: + +```go +func (p *MyInt) Ptr() *MyInt { + return p +} +``` + +在目标文件中,Ptr方法名被修饰为`main.(*MyInt).Ptr`,也就是对应汇编中的`·(*MyInt)·Ptr`。不过在Go汇编语言中,星号和小括弧都无法用作函数名字,也就是无法用汇编直接实现接收参数是指针类型的方法。 + +在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号(比如`type.string."hello"`中的双引号),这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。 -TODO diff --git a/ch3-asm/ch3-07-core-type.md b/ch3-asm/ch3-07-core-type.md new file mode 100644 index 0000000..ae69281 --- /dev/null +++ b/ch3-asm/ch3-07-core-type.md @@ -0,0 +1,3 @@ +# 3.7. Go核心对象结构(TODO) + +TODO diff --git a/ch3-asm/ch3-07-faq.md b/ch3-asm/ch3-07-faq.md new file mode 100644 index 0000000..c1155da --- /dev/null +++ b/ch3-asm/ch3-07-faq.md @@ -0,0 +1,3 @@ +# 3.9. 补充说明 + +得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能或禁制的场合能够通过Go汇编突破限制。这只是一个开始,后续版本会继续完善。 diff --git a/ch3-asm/ch3-07-funcdata-pcdata.md b/ch3-asm/ch3-07-funcdata-pcdata.md deleted file mode 100644 index 85044dc..0000000 --- a/ch3-asm/ch3-07-funcdata-pcdata.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.6. FUNCDATA和PCDATA(TODO) - -TODO diff --git a/ch3-asm/ch3-08-c-preprocessors.md b/ch3-asm/ch3-08-c-preprocessors.md deleted file mode 100644 index 15cb2ae..0000000 --- a/ch3-asm/ch3-08-c-preprocessors.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.8. C预处理器(TODO) - -TODO diff --git a/ch3-asm/ch3-08-runtime-func.md b/ch3-asm/ch3-08-runtime-func.md new file mode 100644 index 0000000..dd8ea83 --- /dev/null +++ b/ch3-asm/ch3-08-runtime-func.md @@ -0,0 +1,5 @@ +# 3.8. runtime内置函数(TODO) + +TODO + + diff --git a/ch3-asm/ch3-09-call-c-leaf-func.md b/ch3-asm/ch3-09-call-c-leaf-func.md new file mode 100644 index 0000000..7237d00 --- /dev/null +++ b/ch3-asm/ch3-09-call-c-leaf-func.md @@ -0,0 +1,3 @@ +# 3.9. 调用C函数(TODO) + +TODO diff --git a/ch3-asm/ch3-09-core-type.md b/ch3-asm/ch3-09-core-type.md deleted file mode 100644 index c5ee6a5..0000000 --- a/ch3-asm/ch3-09-core-type.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.9. Go核心对象结构(TODO) - -TODO diff --git a/ch3-asm/ch3-10-avx-sse-jit.md b/ch3-asm/ch3-10-avx-sse-jit.md new file mode 100644 index 0000000..8ce5fac --- /dev/null +++ b/ch3-asm/ch3-10-avx-sse-jit.md @@ -0,0 +1,3 @@ +# 3.10. AVX/SSE/JIT高级优化(TODO) + +TODO diff --git a/ch3-asm/ch3-10-runtime-func.md b/ch3-asm/ch3-10-runtime-func.md deleted file mode 100644 index 59c32d0..0000000 --- a/ch3-asm/ch3-10-runtime-func.md +++ /dev/null @@ -1,5 +0,0 @@ -# 3.10. runtime内置函数(TODO) - -TODO - - diff --git a/ch3-asm/ch3-11-arm.md b/ch3-asm/ch3-11-arm.md new file mode 100644 index 0000000..ac798d7 --- /dev/null +++ b/ch3-asm/ch3-11-arm.md @@ -0,0 +1,3 @@ +# 3.11. ARM汇编(TODO) + +TODO diff --git a/ch3-asm/ch3-11-call-c-leaf-func.md b/ch3-asm/ch3-11-call-c-leaf-func.md deleted file mode 100644 index 893a4d6..0000000 --- a/ch3-asm/ch3-11-call-c-leaf-func.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.11. 调用C函数(TODO) - -TODO diff --git a/ch3-asm/ch3-12-avx-sse-jit.md b/ch3-asm/ch3-12-avx-sse-jit.md deleted file mode 100644 index 5933285..0000000 --- a/ch3-asm/ch3-12-avx-sse-jit.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.12. AVX/SSE/JIT高级优化(TODO) - -TODO diff --git a/ch3-asm/ch3-13-arm.md b/ch3-asm/ch3-13-arm.md deleted file mode 100644 index 67c94f4..0000000 --- a/ch3-asm/ch3-13-arm.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.14. ARM汇编(TODO) - -TODO diff --git a/ch3-asm/ch3-14-faq.md b/ch3-asm/ch3-14-faq.md deleted file mode 100644 index 883844f..0000000 --- a/ch3-asm/ch3-14-faq.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.14. 补充说明(TODO) - -TODO diff --git a/ch4-mobile/readme.md b/ch4-mobile/readme.md deleted file mode 100644 index e874579..0000000 --- a/ch4-mobile/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# 第四章 移动平台 - -TODO diff --git a/ch4-rpc/ch4-01-rpc-intro.md b/ch4-rpc/ch4-01-rpc-intro.md new file mode 100644 index 0000000..34be2a5 --- /dev/null +++ b/ch4-rpc/ch4-01-rpc-intro.md @@ -0,0 +1,3 @@ +# 4.1. RPC入门 + +TODO diff --git a/ch4-rpc/ch4-02-pb-intro.md b/ch4-rpc/ch4-02-pb-intro.md new file mode 100644 index 0000000..6fc2b55 --- /dev/null +++ b/ch4-rpc/ch4-02-pb-intro.md @@ -0,0 +1,3 @@ +# 4.2. Protobuf简介 + +TODO diff --git a/ch4-rpc/ch4-03-protorpc.md b/ch4-rpc/ch4-03-protorpc.md new file mode 100644 index 0000000..fa420d4 --- /dev/null +++ b/ch4-rpc/ch4-03-protorpc.md @@ -0,0 +1,3 @@ +# 4.3. protorpc + +TODO diff --git a/ch4-rpc/ch4-04-grpc.md b/ch4-rpc/ch4-04-grpc.md new file mode 100644 index 0000000..711c6ac --- /dev/null +++ b/ch4-rpc/ch4-04-grpc.md @@ -0,0 +1,3 @@ +# 4.4. grpc + +TODO diff --git a/ch4-rpc/ch4-05-reverse-rpc.md b/ch4-rpc/ch4-05-reverse-rpc.md new file mode 100644 index 0000000..a6c4413 --- /dev/null +++ b/ch4-rpc/ch4-05-reverse-rpc.md @@ -0,0 +1,3 @@ +# 4.5. 反向rpc + +TODO diff --git a/ch4-rpc/ch4-06-pb-option.md b/ch4-rpc/ch4-06-pb-option.md new file mode 100644 index 0000000..8d498ae --- /dev/null +++ b/ch4-rpc/ch4-06-pb-option.md @@ -0,0 +1,3 @@ +# 4.6. Protobuf扩展 + +TODO diff --git a/ch4-rpc/ch4-07-pb-rpc.md b/ch4-rpc/ch4-07-pb-rpc.md new file mode 100644 index 0000000..531b4be --- /dev/null +++ b/ch4-rpc/ch4-07-pb-rpc.md @@ -0,0 +1,3 @@ +# 4.7. 基于pb的rpc定制 + +TODO diff --git a/ch4-rpc/ch4-08-faq.md b/ch4-rpc/ch4-08-faq.md new file mode 100644 index 0000000..9e43979 --- /dev/null +++ b/ch4-rpc/ch4-08-faq.md @@ -0,0 +1,3 @@ +# 4.8. 补充说明 + +TODO diff --git a/ch4-rpc/readme.md b/ch4-rpc/readme.md new file mode 100644 index 0000000..eba6184 --- /dev/null +++ b/ch4-rpc/readme.md @@ -0,0 +1,3 @@ +# 第四章 RPC和Protobuf + +RPC是远程过程调用的缩写(Remote Procedure Call),通俗地说就是调用远处的一个函数。远处到底有多远呢?可能是同一个文件内的不同函数,也可能是同一个机器的另一个进程的函数,还可能是远在火星好奇号上面的某个秘密方法。因为RPC涉及的函数可能非常之远,远到它们之间说着完全不同的语言,语言将成为两边的沟通障碍。而Protobuf因为支持多种不同的语言(甚至不支持的语言也可以扩展支持),其本身特性也非常方便描述服务的接口(也就是方法列表),因此非常适合作为RPC世界的接口交流语言。本章将讨论RPC的基本用法,以及如何针对不同场景设计自己的RPC服务,以及围绕Protobuf构造的更为庞大的RPC生态。 diff --git a/ch6-web/ch6-01-introduction.md b/ch5-web/ch5-01-introduction.md similarity index 99% rename from ch6-web/ch6-01-introduction.md rename to ch5-web/ch5-01-introduction.md index 0f220d7..4a41464 100644 --- a/ch6-web/ch6-01-introduction.md +++ b/ch5-web/ch5-01-introduction.md @@ -1,4 +1,4 @@ -# 6.1. web 开发简介 +# 5.1. web 开发简介 由于 golang 的 `net/http` 提供了基础的路由函数组合,并且也提供了丰富的功能函数。所以在 golang 社区里有一种观点认为用 golang 写 api 不需要框架。其看法也存在一定的道理,如果你的项目路由在个位数,URI 固定且不通过 URI 来传递参数,那么使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力不从心。例如下面这样的路由: diff --git a/ch6-web/ch6-02-router.md b/ch5-web/ch5-02-router.md similarity index 99% rename from ch6-web/ch6-02-router.md rename to ch5-web/ch5-02-router.md index 0c987b3..532560c 100644 --- a/ch6-web/ch6-02-router.md +++ b/ch5-web/ch5-02-router.md @@ -1,4 +1,4 @@ -# 6.2. router 请求路由 +# 5.2. router 请求路由 在常见的 web 框架中,router 是必备的组件。golang 圈子里 router 也时常被称为 http 的 multiplexer。在上一节中我们通过对 Burrow 代码的简单学习,已经知道如何用 http 标准库中内置的 mux 来完成简单的路由功能了。如果开发 web 系统对路径中带参数没什么兴趣的话,用 http 标准库中的 mux 就可以。 @@ -200,7 +200,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild 子节点的冲突处理很简单,分几种情况: -1. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 false。例如:`GET /user/getAll` 和 `GET /user/:id/getAddr`,或者 `GET /user/*aaa` 和 `GET /user/:id`。 +1. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 false。例如:`GET /user/getAll` 和 `GET /user/:id/getAddr`,或者 `GET /user/*aaa` 和 `GET /user/:id`。 2. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 true,但该父节点的 wildcard 子节点要插入的 wildcard 名字不一样。例如:`GET /user/:id/info` 和 `GET /user/:name/info`。 3. 在插入 catchAll 节点时,父节点的 children 非空。例如:`GET /src/abc` 和 `GET /src/*filename`,或者 `GET /src/:id` 和 `GET /src/*filename`。 4. 在插入 static 节点时,父节点的 wildChild 字段被设置为 true。 diff --git a/ch6-web/ch6-03-middleware.md b/ch5-web/ch5-03-middleware.md similarity index 99% rename from ch6-web/ch6-03-middleware.md rename to ch5-web/ch5-03-middleware.md index a58413b..7dd0338 100644 --- a/ch6-web/ch6-03-middleware.md +++ b/ch5-web/ch5-03-middleware.md @@ -1,4 +1,4 @@ -# 6.3. middleware 中间件 +# 5.3. middleware 中间件 本章将对现在流行的 web 框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。 @@ -267,7 +267,7 @@ logger.go profiler.go => 挂载 pprof 需要的路由,如 /pprof、/pprof/trace 到系统中 realip.go - => 从请求头中读取 X-Forwarded-For 和 X-Real-IP,将 http.Request 中的 RemoteAddr 修改为得到的 RealIP + => 从请求头中读取 X-Forwarded-For 和 X-Real-IP,将 http.Request 中的 RemoteAddr 修改为得到的 RealIP requestid.go => 为本次请求生成单独的 requestid,可一路透传,用来生成分布式调用链路,也可用于在日志中串连单次请求的所有逻辑 timeout.go diff --git a/ch6-web/ch6-04-validator.md b/ch5-web/ch5-04-validator.md similarity index 99% rename from ch6-web/ch6-04-validator.md rename to ch5-web/ch5-04-validator.md index 0fe67fe..bb3c054 100644 --- a/ch6-web/ch6-04-validator.md +++ b/ch5-web/ch5-04-validator.md @@ -1,4 +1,4 @@ -# 6.4. validator 请求校验 +# 5.4. validator 请求校验 社区里曾经有人用这张图来嘲笑 PHP: diff --git a/ch6-web/ch6-05-database.md b/ch5-web/ch5-05-database.md similarity index 99% rename from ch6-web/ch6-05-database.md rename to ch5-web/ch5-05-database.md index 54ed203..0e78d8b 100644 --- a/ch6-web/ch6-05-database.md +++ b/ch5-web/ch5-05-database.md @@ -1,4 +1,4 @@ -# 6.4. Database 和数据库打交道 +# 5.4. Database 和数据库打交道 本节将对 db/sql 官方标准库作一些简单分析,并介绍一些应用比较广泛的开源 orm 和 sql builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。 @@ -151,7 +151,7 @@ num, err := o.QueryTable("cardgroup").Filter("Cards__Card__Name", cardName).All( 除了 limit 的问题,我们再看一遍这个 beego orm 的查询: ```go -num, err := o.QueryTable("cardgroup").Filter("Cards__Card__Name", cardName).All(&cardgroups) +num, err := o.QueryTable("cardgroup").Filter("Cards__Card__Name", cardName).All(&cardgroups) ``` 你可以看得出来这个 Filter 是有表 join 的操作么?当然了,对 beego orm 有过深入使用经验的用户还是会觉得这是在吹毛求疵。但这样的分析想证明的是,orm 想从设计上隐去太多的细节。而方便的代价是其背后的运行完全失控。这样的项目在经过几任维护人员之后,将变得面目全非,难以维护。 @@ -240,4 +240,4 @@ func GetAllByProductIDsAndCustomerID(ctx context.Context, productIDs []uint64, c 像这样的代码,在上线之前把 dao 层的变更集的 const 部分直接拿给 dba 来进行审核,就比较方便了。代码中的 sqlutil.Named 是类似于 sqlx 中的 Named 函数,同时支持 where 表达式中的比较操作符和 in。 -这里为了说明简便,函数写得稍微复杂一些,仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。 \ No newline at end of file +这里为了说明简便,函数写得稍微复杂一些,仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。 diff --git a/ch5-web/ch5-06-ratelimit.md b/ch5-web/ch5-06-ratelimit.md new file mode 100644 index 0000000..42b6af6 --- /dev/null +++ b/ch5-web/ch5-06-ratelimit.md @@ -0,0 +1 @@ +# 5.6. Ratelimit 服务流量限制 diff --git a/ch6-web/ch6-08-layout-of-web-project.md b/ch5-web/ch5-07-layout-of-web-project.md similarity index 99% rename from ch6-web/ch6-08-layout-of-web-project.md rename to ch5-web/ch5-07-layout-of-web-project.md index 281a57c..78d3ebe 100644 --- a/ch6-web/ch6-08-layout-of-web-project.md +++ b/ch5-web/ch5-07-layout-of-web-project.md @@ -1,4 +1,4 @@ -# 6.8. layout 常见大型 web 项目分层 +# 5.7. layout 常见大型 web 项目分层 流行的 web 框架大多数是 MVC 框架,MVC 这个概念最早由 Trygve Reenskaug 在 1978 年提出,为了能够对 GUI 类型的应用进行方便扩展,将程序划分为: diff --git a/ch6-web/ch6-09-interface-and-web.md b/ch5-web/ch5-08-interface-and-web.md similarity index 77% rename from ch6-web/ch6-09-interface-and-web.md rename to ch5-web/ch5-08-interface-and-web.md index 807464f..d5689b0 100644 --- a/ch6-web/ch6-09-interface-and-web.md +++ b/ch5-web/ch5-08-interface-and-web.md @@ -1,4 +1,9 @@ -# 6.8. interface 和 web 编程 +# 5.8. interface 和 table-driven 开发 在项目中我们有可能遇到这样的场景:公司内的基础架构因为技术实力原因,最早是从别人那里借来的 kv 存储方案。随着公司的发展,渐渐有大牛加入,想要甩掉这个借来的包袱自研 kv 存储,但接口与之前的 kv 存储不兼容。接入时需要业务改动接入代码,怎么写代码才能让我的核心业务逻辑不受这些外部资源变化影响呢。 +## 使用 interface 来做抽象 + +## 要不要用继承? + +## table-driven 开发 diff --git a/ch5-web/ch5-09-gated-launch.md b/ch5-web/ch5-09-gated-launch.md new file mode 100644 index 0000000..e4fd872 --- /dev/null +++ b/ch5-web/ch5-09-gated-launch.md @@ -0,0 +1,325 @@ +# 5.9. 灰度发布和 A/B test + +中型的互联网公司往往有着以百万计的用户,而大型互联网公司的系统则可能要服务千万级甚至亿级的用户需求。大型系统的请求流入往往是源源不断的,任何风吹草动,都一定会有最终用户感受得到。例如你的系统在上线途中会拒绝一些上游过来的请求,而这时候依赖你的系统没有做任何容错,那么这个错误就会一直向上抛出,直到触达最终用户。形成一次对用户切切实实的伤害。这种伤害可能是在用户的 app 上弹出一个让用户摸不着头脑的诡异字符串,用户只要刷新一下页面就可以忘记这件事。但也可能会让正在心急如焚地和几万竞争对手同时抢夺秒杀商品的用户,因为代码上的小问题,丧失掉了先发优势,与自己蹲了几个月的心仪产品失之交臂。对用户的伤害有多大,取决于你的系统对于你的用户来说有多重要。 + +不管怎么说,在大型系统中容错是重要的,能够让系统按百分比,分批次到达最终用户,也是很重要的。虽然当今的互联网公司系统,名义上会说自己上线前都经过了充分慎重严格的测试,但就算它们真得做到了,代码的 bug 总是在所难免的。即使代码没有 bug,分布式服务之间的协作也是可能出现“逻辑”上的非技术问题的。 + +这时候,灰度发布就显得非常重要了,灰度发布也称为金丝雀发布,传说 17 世纪的英国矿井工人发现金丝雀对瓦斯气体非常敏感,瓦斯达到一定浓度时,金丝雀即会死亡,但金丝雀的致死量瓦斯对人并不致死,因此金丝雀被用来当成他们的瓦斯检测工具。互联网系统的灰度发布一般通过两种方式实现: + +1. 通过分批次部署实现灰度发布 +2. 通过业务规则进行灰度发布 + +在对系统的旧功能进行升级迭代时,第一种方式用的比较多。新功能上线时,第二种方式用的比较多。当然,对比较重要的老功能进行较大幅度的修改时,一般也会选择按业务规则来进行发布,因为直接全量开放给所有用户风险实在太大。 + +## 通过分批次部署实现灰度发布 + +假如服务部署在 15 个实例(可能是物理机,也可能是容器)上,我们把这 7 个实例分为三组,按照先后顺序,分别有 1-2-4-8 台机器,保证每次扩展时大概都是二倍的关系。 + +``` +┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ +│ │ │ │ │ │ │┌─────┐│ +│ │ │ │ │ │ │└─────┘│ +│ │ │ │ │ │ │┌─────┐│ +│ │ │ │ │ │ │└─────┘│ +│ │ │ │ │ │ │┌─────┐│ +│ │ │ │ │ │ │└─────┘│ +│ │ │ │ │ │ │┌─────┐│ +│ │ │ │ │ │ │└─────┘│ +│ │ │ │ │┌─────┐│ │┌─────┐│ +│ │ │ │ │└─────┘│ │└─────┘│ +│ │ │ │ │┌─────┐│ │┌─────┐│ +│ │ │ │ │└─────┘│ │└─────┘│ +│ │ │┌─────┐│ │┌─────┐│ │┌─────┐│ +│ │ │└─────┘│ │└─────┘│ │└─────┘│ +│┌─────┐│ │┌─────┐│ │┌─────┐│ │┌─────┐│ +│└─────┘│ │└─────┘│ │└─────┘│ │└─────┘│ +└───────┘ └───────┘ └───────┘ └───────┘ + + + group1 group2 group3 group4 + +``` + +为什么要用 2 倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如 1024 台机器,实际上也就只需要 1-2-4-8-16-32-64-128-256-512 部署十次就可以全部部署完毕。 + +这样我们上线最开始影响到的用户在整体用户中占的比例也不大,比如 1000 台机器的服务,我们上线后如果出现问题,也只影响 1/1000 的用户。如果 10 组完全平均分,那一上线立刻就会影响 1/10 的用户,1/10 的业务出问题,那可能对于公司来说就已经是一场不可挽回的事故了。 + +在上线时,最有效的观察手法是查看程序的错误日志,如果较明显的逻辑错误,一般错误日志的滚动速度都会有肉眼可见的增加。这些错误也可以通过 metrics 一类的系统上报给公司内的监控系统,所以在上线过程中,也可以通过观察监控曲线,来判断是否有异常发生。 + +如果有异常情况,首先要做的自然就是回滚了。 + +## 通过业务规则进行灰度发布 + +常见的灰度策略有多种,较为简单的需求,例如我们的策略是要按照千分比来发布,那么我们可以用用户 id、手机号、用户设备信息,等等,来生成一个简单的哈希值,然后再求模,用伪代码表示一下: + +```go +// pass 3/1000 +func passed() bool { + key := hashFunctions(userID) % 1000 + if key <= 2 { + return true + } + + return false +} +``` + +### 可选规则 + +常见的灰度发布系统会有下列规则提供选择: + +1. 按城市发布 +2. 按概率发布 +3. 按百分比发布 +4. 按白名单发布 +5. 按业务线发布 +6. 按 UA 发布(app、web、pc) +7. 按分发渠道发布 + +因为和公司的业务相关,所以城市、业务线、UA、分发渠道这些都可能会被直接编码在系统里,不过功能其实大同小异。 + +按白名单发布比较简单,功能上线时,可能我们希望只有公司内部的员工和测试人员可以访问到新功能,会直接把账号、邮箱写入到白名单,拒绝其它任何账号的访问。 + +按概率发布则是指实现一个简单的函数: + +```go +func isTrue() bool { + return true/false according to the rate provided by user +} +``` + +其可以按照用户指定的概率返回 true/false,当然,true 的概率 + false 的概率 = 100%。这个函数不需要任何输入。 + +按百分比发布,是指实现下面这样的函数: + +```go +func isTrue(phone string) bool { + if hash of phone matches { + return true + } + + return false +} +``` + +这种情况可以按照指定的百分比,返回对应的 true 和 false,和上面的单纯按照概率的区别是这里我们需要调用方提供给我们一个输入参数,我们以该输入参数作为源来计算哈希,并以哈希后的结果来求模,并返回结果。这样可以保证同一个用户的返回结果多次调用是一致的,在下面这种场景下,必须使用这种结果可预期的灰度算法: + +```shell + .---------. + ( user_2 ) + `---------' + +--------+ + .---------. | set.V2 |---------------------+ + ( user_1 ) +--------+ | + `---------' | | + +--------+ | | + +--------------| set.V2 | | | + | +--------+ | | + | | | | + v | | v ++-------------+ | | +-------------+ +| storage_v1 | | | | storage_v2 | ++-------------+ | | +-------------+ + | | | | + | | | | + | | | | + | v | | + | +--------+ | | + +------------->| get.V2 | | | + +--------+ | | + | | + v | + +--------+ | + | get.V2 |<--------------------+ + +--------+ +``` + +## 如何实现一套灰度发布系统 + +前面也提到了,提供给用户的接口大概可以分为和业务绑定的简单灰度判断逻辑。以及输入稍微复杂一些的哈希灰度。我们来分别看看怎么实现这样的灰度系统(函数)。 + +### 业务相关的简单灰度 + +公司内一般都会有公共的城市名字和 id 的映射关系,如果业务只涉及中国国内,那么城市数量不会特别多,且 id 可能都在 10000 范围以内。那么我们只要开辟一个一万大小左右的 bool 数组,就可以满足需求了: + +```go +var cityID2Open = [12000]bool{} + +func init() { + readConfig() + for i:=0;i= 100 { + return true + } + + if rate > 0 && rand.Int(100) > rate { + return true + } + + return false +} +``` + +注意初始化种子。 + +### 哈希算法 + +求哈希可用的算法非常多,比如 md5,crc32,sha1 等等,但我们这里的目的只是为了给这些数据做个映射,并不想要因为计算哈希消耗过多的 cpu,所以现在业界使用较多的算法是 murmurhash,下面是我们对这些常见的 hash 算法的简单 benchmark: + +hash.go: + +```go +package main + +import "crypto/md5" +import "crypto/sha1" +import "github.com/spaolacci/murmur3" + +var str = "hello world" + +func md5Hash() [16]byte { + return md5.Sum([]byte(str)) +} + +func sha1Hash() [20]byte { + return sha1.Sum([]byte(str)) +} + +func murmur32() uint32 { + return murmur3.Sum32([]byte(str)) +} + +func murmur64() uint64 { + return murmur3.Sum64([]byte(str)) +} + +``` + +hash_test.go + +```go +package main + +import "testing" + +func BenchmarkMD5(b *testing.B) { + for i := 0; i < b.N; i++ { + md5Hash() + } +} + +func BenchmarkSHA1(b *testing.B) { + for i := 0; i < b.N; i++ { + sha1Hash() + } +} + +func BenchmarkMurmurHash32(b *testing.B) { + for i := 0; i < b.N; i++ { + murmur32() + } +} + +func BenchmarkMurmurHash64(b *testing.B) { + for i := 0; i < b.N; i++ { + murmur64() + } +} + +``` + +```shell +~/t/g/hash_bench git:master ❯❯❯ go test -bench=. +goos: darwin +goarch: amd64 +BenchmarkMD5-4 10000000 180 ns/op +BenchmarkSHA1-4 10000000 211 ns/op +BenchmarkMurmurHash32-4 50000000 25.7 ns/op +BenchmarkMurmurHash64-4 20000000 66.2 ns/op +PASS +ok _/Users/caochunhui/test/go/hash_bench 7.050s +``` + +可见 murmurhash 相比其它的算法有三倍以上的性能提升。 + +#### 分布是否均匀 + +对于哈希算法来说,性能是一方面的问题,另一方面还要考虑哈希后的值是否分布均匀。 + +我们先以 15810000000 开头,造一千万个和手机号类似的数字,然后将计算后的哈希值分十个桶,并观察计数是否均匀: + +```go +package main + +import ( + "fmt" + + "github.com/spaolacci/murmur3" +) + +var bucketSize = 10 + +func main() { + var bucketMap = map[uint32]int{} + for i := 15000000000; i < 15000000000+10000000; i++ { + hashInt := murmur64(fmt.Sprint(i)) % bucketSize + bucketMap[hashInt]++ + } + fmt.Println(bucketMap) +} + +func murmur32(p string) uint64 { + return murmur3.Sum64([]byte(p)) +} +``` + +```shell +map[7:999475 5:1000359 1:999945 6:1000200 3:1000193 9:1000765 2:1000044 4:1000343 8:1000823 0:997853] +``` + +偏差基本都在 1/100 以内,是可以接受的。 diff --git a/ch6-web/ch6-11-service-discovery.md b/ch5-web/ch5-10-service-discovery.md similarity index 61% rename from ch6-web/ch6-11-service-discovery.md rename to ch5-web/ch5-10-service-discovery.md index c2d2b9d..0557886 100644 --- a/ch6-web/ch6-11-service-discovery.md +++ b/ch5-web/ch5-10-service-discovery.md @@ -1,10 +1,12 @@ -# 6.11. Service Discovery 服务发现 +# 5.10. Service Discovery 服务发现 在微服务架构中,服务之间是存在依赖的。例如在订单系统中创建订单时,需要对用户信息做快照,这时候也就意味着这个流程要依赖: 订单、用户两个系统。当前大型网站的语境下,多服务分布式共存,单个服务也可能会跑在多台物理/虚拟机上。所以即使你知道你需要依赖的是“订单服务”这个具体的服务,实际面对的仍然是多个 ip+port 组成的集群。因此你需要: 1. 通过“订单服务”这个名字找到它对应的 ip+port 列表;2. 决定把这个请求发到哪一个 ip+port 上的订单服务。 ip+port 的组合往往被称为 endpoint。通过“订单服务”去找到这些 endpoint 的过程,叫做服务发现。选择把请求发送给哪一台机器,以最大化利用下游机器的过程,叫做负载均衡。本节主要讨论服务发现。 -## 为什么不用 ip+port 直接连依赖服务? +## 为什么不把 ip+port 写死在自己的配置文件中 + +TODO 从系统的演化角度来讲,加图 在大多数公司发展初期,物理机器比较少,内网 ip 也很少。一些创业公司虽然开发人员众多,但因为业务限制,每一个服务的 QPS 都不高。因此确实有很多公司服务之间是通过 ip+port 来进行相互调用的。再原始一些的话,甚至可能所有服务都在一个工程下,那也就没有什么依赖问题了。 @@ -58,11 +60,83 @@ redis-cli> sadd order_service.http 100.10.100.11:1002 1. 客户端主动的故障摘除 2. 客户端被动故障摘除。 -主动的故障摘除是指,我作为依赖其它人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。 +主动的故障摘除是指,我作为依赖其它人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。下面是一种根据请求时是否出错,对相应的服务节点进行投票的一个例子: ```go +// 对下游的请求正常返回时: +node := getNodeFromPool() + +resp, err := remoteRPC(ctx, params) + +if err != nil { + node.Vote(status.Healthy) +} else { + node.Vote(status.Unhealthy) +} +``` + +在节点管理时,会对 Unhealthy 过多的节点进行摘除,这个过程可以在 Unhealthy 的数量超过一定的阈值之后自动触发,也就是在 Vote 函数中实现即可。 + +如果你选择用应用层心跳,那需要下游提供 healthcheck 的接口,这个接口一般就简单返回 success 就可以了。上游要做的事情就是每隔一小段时间,去请求 healthcheck 接口,如果超时、响应失败,那么就把该节点摘除: + +```go +healthcheck := func(endpoint string) { + for { + time.Sleep(time.Second * 10) + + resp, err := callRemoteHealthcheckAPI(endpoint) + if err != nil { + dropThisAPINode() + } + } +}() + +for _, endpoint := range endpointList { + go healthcheck(endpoint) +} ``` 被动故障摘除,顾名思义。依赖出问题了要别人通知我。这个通知一般通过服务注册中心发给我。 被动故障摘除,最早的解决方案是 zookeeper 的 ephemeral node,java 技术栈的服务发现框架很多是基于此来做故障服务节点摘除。 + +比如我们是电商的平台部的订单系统,那么可以建立类似这样的永久节点: + +```shell +/platform/order-system/create-order-service-http +``` + +然后把我们的 endpoints 作为临时节点,建立在上述节点之下: + +```shell +ls /platform/order-system/create-order-service-http + +[] +``` + +当与 zk 断开连接时,注册在该节点下的临时节点也会消失,即实现了服务节点故障时的被动摘除。该事件也会通知 watch 该节点的所有监视方。 + +用代码来实现一下上面的几个逻辑: + +```go +``` + +有了临时节点、监视功能、故障时的自动摘除功能,我们实现一套服务发现以及故障节点摘除的基本元件也就齐全了。 + +目前在企业级应用中,上述几种故障摘除方案都是存在的。读者朋友可以根据自己公司的发展阶段,灵活选用对应的方案。需要明白的一点是,并非一定要有 zk、etcd 这样的组件才能完成故障摘除。 + +## 服务发现究竟应该是 CP 还是 AP 系统 + +当前开源的服务发现系统中,使用 zk、etcd 或者 consul,无一例外地都是看中其强一致性的特性。也就是大部分人认为服务发现和分布式系统中的任务协调场景一致,是一个 CP 系统。我们来想想,如果放弃了可用性会导致什么样的结果。 + +在上面提到的几个开源组件中使用的 paxos/zab/raft 协议,若因为网络分区等原因,导致集群节点数量 < n/2 时,会导致整个集群不能对外提供服务。网络分区是分布式系统中常见的错误。如果公司的服务部署在公有云、或者私有云的 docker 环境上,网络问题就更为常见了。一旦发生网络分区,就会导致类似下面这样的场景出现: + +TODO,网络分区示例图 + +如果分区之后再分区,那我们可能都无法从注册中心拿到任何数据了,服务之间的连通性也就无从谈起了。 + +当几千或者上万个服务同时依赖同一个下游服务时,这些服务对应的万级以上的机器实例都需要对服务注册中心的依赖服务的注册节点进行监视。该依赖服务一旦发生 endpoint 变动,就会产生广播风暴。 + +## eureka 新时代的服务发现 + +eureka 是大名鼎鼎的 Netflix 公司对服务发现场景重新思考的结果。和传统的服务发现工具相比,eureka 将 CP 改为了 AP。 diff --git a/ch6-web/ch6-12-load-balance.md b/ch5-web/ch5-11-load-balance.md similarity index 98% rename from ch6-web/ch6-12-load-balance.md rename to ch5-web/ch5-11-load-balance.md index d264a05..20d198e 100644 --- a/ch6-web/ch6-12-load-balance.md +++ b/ch5-web/ch5-11-load-balance.md @@ -1,4 +1,4 @@ -# 6.12. Load-Balance 负载均衡 +# 5.11. Load-Balance 负载均衡 本节将会讨论常见的 web 后端服务之间的负载均衡手段。 @@ -178,3 +178,7 @@ map[6:143275 5:143054 3:143584 2:143031 1:141898 0:142631 4:142527] ``` 分布结果和我们推导出的理论是一致的。 + +## 基于一致性哈希的负载均衡 + +## ketama hash diff --git a/ch5-web/ch5-12-dist-config.md b/ch5-web/ch5-12-dist-config.md new file mode 100644 index 0000000..3c6662d --- /dev/null +++ b/ch5-web/ch5-12-dist-config.md @@ -0,0 +1 @@ +# 5.12. Dist-config 分布式配置服务 diff --git a/ch5-web/ch5-13-circuit-breaker.md b/ch5-web/ch5-13-circuit-breaker.md new file mode 100644 index 0000000..427d780 --- /dev/null +++ b/ch5-web/ch5-13-circuit-breaker.md @@ -0,0 +1 @@ +# 5.13. Circuit-Breaker 熔断保护 diff --git a/ch5-web/ch5-14-monitor.md b/ch5-web/ch5-14-monitor.md new file mode 100644 index 0000000..1707c15 --- /dev/null +++ b/ch5-web/ch5-14-monitor.md @@ -0,0 +1 @@ +# 5.8. Monitor metrics 和服务监控 diff --git a/ch6-web/ch6-15-extend.md b/ch5-web/ch5-15-extend.md similarity index 100% rename from ch6-web/ch6-15-extend.md rename to ch5-web/ch5-15-extend.md diff --git a/ch6-web/ch6-06-rpc.md b/ch5-web/ch5-16-test-and-mock.md similarity index 100% rename from ch6-web/ch6-06-rpc.md rename to ch5-web/ch5-16-test-and-mock.md diff --git a/ch6-web/readme.md b/ch5-web/readme.md similarity index 92% rename from ch6-web/readme.md rename to ch5-web/readme.md index d92531b..3707858 100644 --- a/ch6-web/readme.md +++ b/ch5-web/readme.md @@ -1,4 +1,4 @@ -# 第六章 go 和 web +# 第五章 go 和 web 本章将会阐述 go 在 web 开发方面的现状,并以几个典型的开源 web 框架为例,带大家深入 web 框架本身的执行流程。 diff --git a/ch6-cloud/ch6-01-cloud.md b/ch6-cloud/ch6-01-cloud.md new file mode 100644 index 0000000..98c1722 --- /dev/null +++ b/ch6-cloud/ch6-01-cloud.md @@ -0,0 +1,3 @@ +# 6.1. 云上地鼠 + +TODO diff --git a/ch6-cloud/ch6-02-raft.md b/ch6-cloud/ch6-02-raft.md new file mode 100644 index 0000000..0890385 --- /dev/null +++ b/ch6-cloud/ch6-02-raft.md @@ -0,0 +1,3 @@ +# 6.2. Raft协议 + +TODO diff --git a/ch6-cloud/ch6-03-hash.md b/ch6-cloud/ch6-03-hash.md new file mode 100644 index 0000000..30f685e --- /dev/null +++ b/ch6-cloud/ch6-03-hash.md @@ -0,0 +1,3 @@ +# 6.3. 分布式哈希 + +TODO diff --git a/ch6-cloud/ch6-04-queue.md b/ch6-cloud/ch6-04-queue.md new file mode 100644 index 0000000..cf475fe --- /dev/null +++ b/ch6-cloud/ch6-04-queue.md @@ -0,0 +1,3 @@ +# 6.4. 分布式队列 + +TODO diff --git a/ch6-cloud/ch6-05-cache.md b/ch6-cloud/ch6-05-cache.md new file mode 100644 index 0000000..deee771 --- /dev/null +++ b/ch6-cloud/ch6-05-cache.md @@ -0,0 +1,3 @@ +# 6.5. 分布式缓存 + +TODO diff --git a/ch6-cloud/ch6-06-etcd.md b/ch6-cloud/ch6-06-etcd.md new file mode 100644 index 0000000..5dd9489 --- /dev/null +++ b/ch6-cloud/ch6-06-etcd.md @@ -0,0 +1,3 @@ +# 6.6. etcd + +TODO diff --git a/ch6-cloud/ch6-07-confd.md b/ch6-cloud/ch6-07-confd.md new file mode 100644 index 0000000..bdccf7e --- /dev/null +++ b/ch6-cloud/ch6-07-confd.md @@ -0,0 +1,3 @@ +# 6.7. confd + +TODO diff --git a/ch6-cloud/ch6-08-lock.md b/ch6-cloud/ch6-08-lock.md new file mode 100644 index 0000000..f646cb9 --- /dev/null +++ b/ch6-cloud/ch6-08-lock.md @@ -0,0 +1,3 @@ +# 6.8. 分布式锁 + +TODO diff --git a/ch6-cloud/ch6-09-sched.md b/ch6-cloud/ch6-09-sched.md new file mode 100644 index 0000000..f7bd7b9 --- /dev/null +++ b/ch6-cloud/ch6-09-sched.md @@ -0,0 +1,3 @@ +# 6.9. 分布式任务调度系统 + +TODO diff --git a/ch6-cloud/ch6-10-delay-job.md b/ch6-cloud/ch6-10-delay-job.md new file mode 100644 index 0000000..837da81 --- /dev/null +++ b/ch6-cloud/ch6-10-delay-job.md @@ -0,0 +1,3 @@ +# 6.10. 延时任务系统 + +TODO diff --git a/ch6-cloud/ch6-11-k8s.md b/ch6-cloud/ch6-11-k8s.md new file mode 100644 index 0000000..476a990 --- /dev/null +++ b/ch6-cloud/ch6-11-k8s.md @@ -0,0 +1,3 @@ +# 6.11. Kubernetes + +TODO diff --git a/ch6-cloud/ch6-12-faq.md b/ch6-cloud/ch6-12-faq.md new file mode 100644 index 0000000..8bc80e4 --- /dev/null +++ b/ch6-cloud/ch6-12-faq.md @@ -0,0 +1,3 @@ +# 6.12. 补充说明 + +TODO diff --git a/ch6-cloud/readme.md b/ch6-cloud/readme.md new file mode 100644 index 0000000..78a604f --- /dev/null +++ b/ch6-cloud/readme.md @@ -0,0 +1,4 @@ +# 第六章 分布式系统 + +Go语言号称是互联网时代的C语言。现在的互联网系统已经不是以前的一个主机搞定一切的时代,互联网时代的服务后台有大量的分布式系统构成,任何单一后台服务器节点的故障并不会导致整个系统的停机。同时以青云、阿里云、腾讯云为代表的云厂商崛起标志着云时代的到来,在云时代分布式编程将成为一个基本技能。而基于Go语言构建的Docker、K8s等系统正是推动了云时代的提前到来。本章将简单讨论如何使用Go语言开发各种分布式系统。 + diff --git a/ch6-web/ch6-07-ratelimit.md b/ch6-web/ch6-07-ratelimit.md deleted file mode 100644 index 1afe162..0000000 --- a/ch6-web/ch6-07-ratelimit.md +++ /dev/null @@ -1 +0,0 @@ -# 6.7. Ratelimit 服务流量限制 diff --git a/ch6-web/ch6-10-dist-config.md b/ch6-web/ch6-10-dist-config.md deleted file mode 100644 index af8adc7..0000000 --- a/ch6-web/ch6-10-dist-config.md +++ /dev/null @@ -1 +0,0 @@ -# 6.8. Dist-config 分布式配置服务 \ No newline at end of file diff --git a/ch6-web/ch6-13-circuit-breaker.md b/ch6-web/ch6-13-circuit-breaker.md deleted file mode 100644 index 29bacb3..0000000 --- a/ch6-web/ch6-13-circuit-breaker.md +++ /dev/null @@ -1 +0,0 @@ -# 6.9. Circuit-Breaker 熔断保护 diff --git a/ch6-web/ch6-14-monitor.md b/ch6-web/ch6-14-monitor.md deleted file mode 100644 index 2262563..0000000 --- a/ch6-web/ch6-14-monitor.md +++ /dev/null @@ -1 +0,0 @@ -# 6.8. Monitor metrics 和服务监控 diff --git a/ch7-ast/readme.md b/ch7-ast/readme.md new file mode 100644 index 0000000..d26fed0 --- /dev/null +++ b/ch7-ast/readme.md @@ -0,0 +1,3 @@ +# 第七章 Go 和 AST + +AST是抽象语法树的缩写(abstract syntax tree),一般可以用一个树型结构表示源代码的抽象语法结构。比如一个算术表达式可以用AST表示,if分支结构、for循环结构也可以用AST表示。因为树是一个任意分叉的,AST也可以非常容易if分支、for循环等嵌套的结构。了解AST不仅仅可以加深对语言本身的理解,基于AST也可以做很多有意义的事情(比如分析某类型的BUG、进行某种优化等)。更让人兴奋的是Go语言标准库已经内置了强大易用的AST库,让我们了解一下这种神秘的技术吧。 diff --git a/ch7-tools/ch7-12-awesome-go.md b/ch7-tools/ch7-12-awesome-go.md deleted file mode 100644 index e69de29..0000000 diff --git a/ch6-web/ch6-16-test-and-mock.md b/ch8-tools/ch8-01-json2go.md similarity index 100% rename from ch6-web/ch6-16-test-and-mock.md rename to ch8-tools/ch8-01-json2go.md diff --git a/ch7-tools/ch7-01-json2go.md b/ch8-tools/ch8-02-snippets.md similarity index 100% rename from ch7-tools/ch7-01-json2go.md rename to ch8-tools/ch8-02-snippets.md diff --git a/ch7-tools/ch7-02-snippets.md b/ch8-tools/ch8-03-astviewer.md similarity index 100% rename from ch7-tools/ch7-02-snippets.md rename to ch8-tools/ch8-03-astviewer.md diff --git a/ch7-tools/ch7-03-astviewer.md b/ch8-tools/ch8-04-go-prompt.md similarity index 100% rename from ch7-tools/ch7-03-astviewer.md rename to ch8-tools/ch8-04-go-prompt.md diff --git a/ch7-tools/ch7-04-go-prompt.md b/ch8-tools/ch8-05-ginkgo.md similarity index 100% rename from ch7-tools/ch7-04-go-prompt.md rename to ch8-tools/ch8-05-ginkgo.md diff --git a/ch7-tools/ch7-05-ginkgo.md b/ch8-tools/ch8-06-swag.md similarity index 100% rename from ch7-tools/ch7-05-ginkgo.md rename to ch8-tools/ch8-06-swag.md diff --git a/ch7-tools/ch7-06-swag.md b/ch8-tools/ch8-07-sqlparser.md similarity index 100% rename from ch7-tools/ch7-06-swag.md rename to ch8-tools/ch8-07-sqlparser.md diff --git a/ch7-tools/ch7-07-sqlparser.md b/ch8-tools/ch8-08-go-valuate.md similarity index 100% rename from ch7-tools/ch7-07-sqlparser.md rename to ch8-tools/ch8-08-go-valuate.md diff --git a/ch7-tools/ch7-08-go-valuate.md b/ch8-tools/ch8-09-fast-template.md similarity index 100% rename from ch7-tools/ch7-08-go-valuate.md rename to ch8-tools/ch8-09-fast-template.md diff --git a/ch7-tools/ch7-09-fast-template.md b/ch8-tools/ch8-10-struct-copy.md similarity index 100% rename from ch7-tools/ch7-09-fast-template.md rename to ch8-tools/ch8-10-struct-copy.md diff --git a/ch7-tools/ch7-10-struct-copy.md b/ch8-tools/ch8-11-gometalinter.md similarity index 100% rename from ch7-tools/ch7-10-struct-copy.md rename to ch8-tools/ch8-11-gometalinter.md diff --git a/ch7-tools/ch7-11-gometalinter.md b/ch8-tools/ch8-12-awesome-go.md similarity index 100% rename from ch7-tools/ch7-11-gometalinter.md rename to ch8-tools/ch8-12-awesome-go.md diff --git a/ch7-tools/readme.md b/ch8-tools/readme.md similarity index 94% rename from ch7-tools/readme.md rename to ch8-tools/readme.md index de0baf3..f17dbde 100644 --- a/ch7-tools/readme.md +++ b/ch8-tools/readme.md @@ -1,4 +1,4 @@ -# 第七章 go 和那些生产力工具 +# 第八章 Go和那些生产力工具 在日常开发中我们难免遇到很多重复劳动,程序员的天性使他们更倾向于消灭重复劳动。哪怕花半小时去写脚本,也一定要消灭五分钟的痛苦。这样才能让生活更美好。