1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 20:52:22 +00:00

Merge pull request #1 from chai2010/master

同步
This commit is contained in:
wahaha 2018-06-14 12:29:17 +08:00 committed by GitHub
commit 5fd53adf54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1287 additions and 82 deletions

View File

@ -26,29 +26,43 @@
* [3.1. 快速入门](ch3-asm/ch3-01-basic.md) * [3.1. 快速入门](ch3-asm/ch3-01-basic.md)
* [3.2. 计算机结构](ch3-asm/ch3-02-arch.md) * [3.2. 计算机结构](ch3-asm/ch3-02-arch.md)
* [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md) * [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md)
* [3.4. 函数(Doing)](ch3-asm/ch3-04-func.md) * [3.4. 函数](ch3-asm/ch3-04-func.md)
* [3.5. 控制流(TODO)](ch3-asm/ch3-05-control-flow.md) * [3.5. 控制流](ch3-asm/ch3-05-control-flow.md)
* [3.6. 再论函数(TODO)](ch3-asm/ch3-06-func-again.md) * [3.6. 再论函数](ch3-asm/ch3-06-func-again.md)
* [3.7. FUNCDATA和PCDATA(TODO)](ch3-asm/ch3-07-funcdata-pcdata.md) * [3.7. 补充说明](ch3-asm/ch3-07-faq.md)
* [3.8. C预处理器(TODO)](ch3-asm/ch3-08-c-preprocessors.md) * [第四章 RPC和Protobuf](ch4-rpc/readme.md)
* [3.9. Go核心对象结构(TODO)](ch3-asm/ch3-09-core-type.md) * [4.1. RPC入门(TODO)](ch4-rpc/ch4-01-rpc-intro.md)
* [3.10. runtime内置函数(TODO)](ch3-asm/ch3-10-runtime-func.md) * [4.2. Protobuf简介(TODO)](ch4-rpc/ch4-02-pb-intro.md)
* [3.11. 调用C函数(TODO)](ch3-asm/ch3-11-call-c-leaf-func.md) * [4.3. protorpc(TODO)](ch4-rpc/ch4-03-protorpc.md)
* [3.12. AVX/SSE/JIT高级优化(TODO)](ch3-asm/ch3-12-avx-sse-jit.md) * [4.4. grpc(TODO)](ch4-rpc/ch4-04-grpc.md)
* [3.13. ARM汇编(TODO)](ch3-asm/ch3-13-arm.md) * [4.5. 反向rpc(TODO)](ch4-rpc/ch4-05-reverse-rpc.md)
* [3.14. 补充说明(TODO)](ch3-asm/ch3-14-faq.md) * [4.6. Protobuf扩展(TODO)](ch4-rpc/ch4-06-pb-option.md)
* [第四章 移动平台(TODO)](ch4-mobile/readme.md) * [4.7. 基于pb的rpc定制(TODO)](ch4-rpc/ch4-07-pb-rpc.md)
* [第五章 这是一个坑(TODO)](ch5-wtf/readme.md) * [4.8. 补充说明(TODO)](ch4-rpc/ch4-08-faq.md)
* [第六章 Go和Web](ch6-web/readme.md) * [第五章 Go和Web](ch5-web/readme.md)
* [6.1. Web开发简介](ch6-web/ch6-01-introduction.md) * [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
* [6.2. Router请求路由](ch6-web/ch6-02-router.md) * [5.2. Router请求路由](ch5-web/ch5-02-router.md)
* [6.3. Middleware中间件](ch6-web/ch6-03-middleware.md) * [5.3. Middleware中间件](ch5-web/ch5-03-middleware.md)
* [6.4. Validator请求校验](ch6-web/ch6-04-validator.md) * [5.4. Validator请求校验](ch5-web/ch5-04-validator.md)
* [6.5. Database和数据库打交道](ch6-web/ch6-05-database.md) * [5.5. Database和数据库打交道](ch5-web/ch5-05-database.md)
* [6.8. Layout大型web项目分层](ch6-web/ch6-08-layout-of-web-project.md) * [5.7. Layout大型web项目分层](ch5-web/ch5-07-layout-of-web-project.md)
* [6.12. Load-balance负载均衡](ch6-web/ch6-12-load-balance.md) * [5.9. 灰度发布和 A/B test](ch5-web/ch5-09-gated-launch.md)
* [第七章 Go和那些生产力工具](ch7-tools/readme.md) * [5.11. Load-balance负载均衡](ch5-web/ch5-11-load-balance.md)
* [7.1. json2go](ch7-tools/ch7-01-json2go.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) * [附录](appendix/readme.md)
* [附录A: Go语言常见坑](appendix/appendix-a-trap.md) * [附录A: Go语言常见坑](appendix/appendix-a-trap.md)
* [附录B: 参考资料](appendix/appendix-b-ref.md) * [附录B: 参考资料](appendix/appendix-b-ref.md)

View File

@ -1,3 +1,4 @@
# 附录C作者简介 # 附录C作者简介
- [柴树杉网络IDchai2010](https://github.com/chai2010) 国内第一批Go语言爱好者创建了最早的QQ讨论组和golang-china邮件列表组织 [Go语言官方文档](https://github.com/golang-china) 和 [《Go语言圣经》](https://github.com/golang-china/gopl-zh) 的翻译工作Go语言代码的贡献者。 - [柴树杉网络IDchai2010](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

View File

@ -268,7 +268,7 @@ fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
```go ```go
func forOnString(s string, forBody func(i int, r rune)) { func forOnString(s string, forBody func(i int, r rune)) {
for i := 0; len(s) > 0; { for i := 0; len(s) > 0; {
r, size := utf8.DecodeRuneInString(str) r, size := utf8.DecodeRuneInString(s)
forBody(i, r) forBody(i, r)
s = s[size:] s = s[size:]
i += size i += size

View File

@ -43,6 +43,8 @@ func worker(wg *sync.WaitGroup) {
} }
func main() { func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg) go worker(&wg)
go worker(&wg) go worker(&wg)
wg.Wait() wg.Wait()
@ -84,7 +86,7 @@ func main() {
`atomic.AddUint64`函数调用保证了`total`的读取、更新和保存是一个原子操作,因此在多线程中访问也是安全的。 `atomic.AddUint64`函数调用保证了`total`的读取、更新和保存是一个原子操作,因此在多线程中访问也是安全的。
原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态通过降低互斥锁的次数来提高性能。 原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。
```go ```go
type singleton struct {} type singleton struct {}

View File

@ -1,5 +1,250 @@
# 3.4. 函数(Doing) # 3.4. 函数
终于到函数了因为Go汇编语言中可以也建议通过Go语言来定义全局变量那么剩下的也就是函数了。只有掌握了汇编函数的基本用法才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 终于到函数了因为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
```
因为预处理器可以通过条件编译针对不同的平台定义宏的实现,这样可以简化平台带来的差异。

View File

@ -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变量对应的内存设置为10AX也是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寄存器初始化为0DX对应变量i循环的迭代变量。在LOOP_IF标号对应的指令部分使用CMPQ指令比较AX和AX如果循环没有结束则跳转到LOOP_BODY部分否则跳转到LOOP_END部分结束循环。在LOOP_BODY部分更新迭代变量并且执行循环体中到累加语句然后直接跳转到LOOP_IF部分进入下一轮循环条件判断。LOOP_END标号之后就是返回返回累加结果到语句。
循环是最复杂到控制流,循环中隐含了分支和跳转语句。掌握了循环到下方基本也就掌握了汇编语言到写法。掌握规律之后,其实汇编语言编程会变得异常简单。

View File

@ -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

View File

@ -0,0 +1,3 @@
# 3.7. Go核心对象结构(TODO)
TODO

3
ch3-asm/ch3-07-faq.md Normal file
View File

@ -0,0 +1,3 @@
# 3.9. 补充说明
得益于Go语言的设计Go汇编语言的优势也非常明显跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足它不是一个独立的语言底层需要依赖Go语言甚至操作系统很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单但是汇编语言是一个比较大的话题大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门在看到底层汇编代码的时候不会一头雾水在某些遇到性能或禁制的场合能够通过Go汇编突破限制。这只是一个开始后续版本会继续完善。

View File

@ -1,3 +0,0 @@
# 3.6. FUNCDATA和PCDATA(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.8. C预处理器(TODO)
TODO

View File

@ -0,0 +1,5 @@
# 3.8. runtime内置函数(TODO)
TODO

View File

@ -0,0 +1,3 @@
# 3.9. 调用C函数(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.9. Go核心对象结构(TODO)
TODO

View File

@ -0,0 +1,3 @@
# 3.10. AVX/SSE/JIT高级优化(TODO)
TODO

View File

@ -1,5 +0,0 @@
# 3.10. runtime内置函数(TODO)
TODO

3
ch3-asm/ch3-11-arm.md Normal file
View File

@ -0,0 +1,3 @@
# 3.11. ARM汇编(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.11. 调用C函数(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.12. AVX/SSE/JIT高级优化(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.14. ARM汇编(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 3.14. 补充说明(TODO)
TODO

View File

@ -1,3 +0,0 @@
# 第四章 移动平台
TODO

View File

@ -0,0 +1,3 @@
# 4.1. RPC入门
TODO

View File

@ -0,0 +1,3 @@
# 4.2. Protobuf简介
TODO

View File

@ -0,0 +1,3 @@
# 4.3. protorpc
TODO

3
ch4-rpc/ch4-04-grpc.md Normal file
View File

@ -0,0 +1,3 @@
# 4.4. grpc
TODO

View File

@ -0,0 +1,3 @@
# 4.5. 反向rpc
TODO

View File

@ -0,0 +1,3 @@
# 4.6. Protobuf扩展
TODO

3
ch4-rpc/ch4-07-pb-rpc.md Normal file
View File

@ -0,0 +1,3 @@
# 4.7. 基于pb的rpc定制
TODO

3
ch4-rpc/ch4-08-faq.md Normal file
View File

@ -0,0 +1,3 @@
# 4.8. 补充说明
TODO

3
ch4-rpc/readme.md Normal file
View File

@ -0,0 +1,3 @@
# 第四章 RPC和Protobuf
RPC是远程过程调用的缩写Remote Procedure Call通俗地说就是调用远处的一个函数。远处到底有多远呢可能是同一个文件内的不同函数也可能是同一个机器的另一个进程的函数还可能是远在火星好奇号上面的某个秘密方法。因为RPC涉及的函数可能非常之远远到它们之间说着完全不同的语言语言将成为两边的沟通障碍。而Protobuf因为支持多种不同的语言甚至不支持的语言也可以扩展支持其本身特性也非常方便描述服务的接口也就是方法列表因此非常适合作为RPC世界的接口交流语言。本章将讨论RPC的基本用法以及如何针对不同场景设计自己的RPC服务以及围绕Protobuf构造的更为庞大的RPC生态。

View File

@ -1,4 +1,4 @@
# 6.1. web 开发简介 # 5.1. web 开发简介
由于 golang 的 `net/http` 提供了基础的路由函数组合,并且也提供了丰富的功能函数。所以在 golang 社区里有一种观点认为用 golang 写 api 不需要框架。其看法也存在一定的道理如果你的项目路由在个位数URI 固定且不通过 URI 来传递参数,那么使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力不从心。例如下面这样的路由: 由于 golang 的 `net/http` 提供了基础的路由函数组合,并且也提供了丰富的功能函数。所以在 golang 社区里有一种观点认为用 golang 写 api 不需要框架。其看法也存在一定的道理如果你的项目路由在个位数URI 固定且不通过 URI 来传递参数,那么使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力不从心。例如下面这样的路由:

View File

@ -1,4 +1,4 @@
# 6.2. router 请求路由 # 5.2. router 请求路由
在常见的 web 框架中router 是必备的组件。golang 圈子里 router 也时常被称为 http 的 multiplexer。在上一节中我们通过对 Burrow 代码的简单学习,已经知道如何用 http 标准库中内置的 mux 来完成简单的路由功能了。如果开发 web 系统对路径中带参数没什么兴趣的话,用 http 标准库中的 mux 就可以。 在常见的 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` 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` 3. 在插入 catchAll 节点时,父节点的 children 非空。例如:`GET /src/abc``GET /src/*filename`,或者 `GET /src/:id``GET /src/*filename`
4. 在插入 static 节点时,父节点的 wildChild 字段被设置为 true。 4. 在插入 static 节点时,父节点的 wildChild 字段被设置为 true。

View File

@ -1,4 +1,4 @@
# 6.3. middleware 中间件 # 5.3. middleware 中间件
本章将对现在流行的 web 框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。 本章将对现在流行的 web 框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。
@ -267,7 +267,7 @@ logger.go
profiler.go profiler.go
=> 挂载 pprof 需要的路由,如 /pprof、/pprof/trace 到系统中 => 挂载 pprof 需要的路由,如 /pprof、/pprof/trace 到系统中
realip.go 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.go
=> 为本次请求生成单独的 requestid可一路透传用来生成分布式调用链路也可用于在日志中串连单次请求的所有逻辑 => 为本次请求生成单独的 requestid可一路透传用来生成分布式调用链路也可用于在日志中串连单次请求的所有逻辑
timeout.go timeout.go

View File

@ -1,4 +1,4 @@
# 6.4. validator 请求校验 # 5.4. validator 请求校验
社区里曾经有人用这张图来嘲笑 PHP 社区里曾经有人用这张图来嘲笑 PHP

View File

@ -1,4 +1,4 @@
# 6.4. Database 和数据库打交道 # 5.4. Database 和数据库打交道
本节将对 db/sql 官方标准库作一些简单分析,并介绍一些应用比较广泛的开源 orm 和 sql builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。 本节将对 db/sql 官方标准库作一些简单分析,并介绍一些应用比较广泛的开源 orm 和 sql builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。
@ -151,7 +151,7 @@ num, err := o.QueryTable("cardgroup").Filter("Cards__Card__Name", cardName).All(
除了 limit 的问题,我们再看一遍这个 beego orm 的查询: 除了 limit 的问题,我们再看一遍这个 beego orm 的查询:
```go ```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 想从设计上隐去太多的细节。而方便的代价是其背后的运行完全失控。这样的项目在经过几任维护人员之后,将变得面目全非,难以维护。 你可以看得出来这个 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。 像这样的代码,在上线之前把 dao 层的变更集的 const 部分直接拿给 dba 来进行审核,就比较方便了。代码中的 sqlutil.Named 是类似于 sqlx 中的 Named 函数,同时支持 where 表达式中的比较操作符和 in。
这里为了说明简便,函数写得稍微复杂一些,仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。 这里为了说明简便,函数写得稍微复杂一些,仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。

View File

@ -0,0 +1 @@
# 5.6. Ratelimit 服务流量限制

View File

@ -1,4 +1,4 @@
# 6.8. layout 常见大型 web 项目分层 # 5.7. layout 常见大型 web 项目分层
流行的 web 框架大多数是 MVC 框架MVC 这个概念最早由 Trygve Reenskaug 在 1978 年提出,为了能够对 GUI 类型的应用进行方便扩展,将程序划分为: 流行的 web 框架大多数是 MVC 框架MVC 这个概念最早由 Trygve Reenskaug 在 1978 年提出,为了能够对 GUI 类型的应用进行方便扩展,将程序划分为:

View File

@ -1,4 +1,9 @@
# 6.8. interface 和 web 编程 # 5.8. interface 和 table-driven 开发
在项目中我们有可能遇到这样的场景:公司内的基础架构因为技术实力原因,最早是从别人那里借来的 kv 存储方案。随着公司的发展,渐渐有大牛加入,想要甩掉这个借来的包袱自研 kv 存储,但接口与之前的 kv 存储不兼容。接入时需要业务改动接入代码,怎么写代码才能让我的核心业务逻辑不受这些外部资源变化影响呢。 在项目中我们有可能遇到这样的场景:公司内的基础架构因为技术实力原因,最早是从别人那里借来的 kv 存储方案。随着公司的发展,渐渐有大牛加入,想要甩掉这个借来的包袱自研 kv 存储,但接口与之前的 kv 存储不兼容。接入时需要业务改动接入代码,怎么写代码才能让我的核心业务逻辑不受这些外部资源变化影响呢。
## 使用 interface 来做抽象
## 要不要用继承?
## table-driven 开发

View File

@ -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<len(cityID2Open);i++ {
if city i is opened in configs {
cityID2Open = true
}
}
}
func isPassed(cityID int) bool {
return cityID2Open[cityID]
}
```
如果公司给 cityID 赋的值比较大,那么我们可以考虑用 map 来存储映射关系map 的查询比数组稍慢,但扩展会灵活一些:
```go
var cityID2Open = map[int]struct{}{}
func init() {
readConfig()
for _, city := range openCities {
cityID2Open[city] = struct{}{}
}
}
func isPassed(cityID int) bool {
if _, ok := cityID2Open[cityID]; ok {
return true
}
return false
}
```
按白名单、按业务线、按 UA、按分发渠道发布本质上和按城市发布是一样的这里就不再赘述了。
按概率发布稍微特殊一些,不过不考虑输入实现起来也很简单:
```go
func init() {
rand.Seed(time.Now().UnixNano())
}
// rate 为 0~100
func isPassed(rate int) bool {
if rate >= 100 {
return true
}
if rate > 0 && rand.Int(100) > rate {
return true
}
return false
}
```
注意初始化种子。
### 哈希算法
求哈希可用的算法非常多,比如 md5crc32sha1 等等,但我们这里的目的只是为了给这些数据做个映射,并不想要因为计算哈希消耗过多的 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 以内,是可以接受的。

View File

@ -1,10 +1,12 @@
# 6.11. Service Discovery 服务发现 # 5.10. Service Discovery 服务发现
在微服务架构中,服务之间是存在依赖的。例如在订单系统中创建订单时,需要对用户信息做快照,这时候也就意味着这个流程要依赖: 订单、用户两个系统。当前大型网站的语境下,多服务分布式共存,单个服务也可能会跑在多台物理/虚拟机上。所以即使你知道你需要依赖的是“订单服务”这个具体的服务,实际面对的仍然是多个 ip+port 组成的集群。因此你需要: 1. 通过“订单服务”这个名字找到它对应的 ip+port 列表2. 决定把这个请求发到哪一个 ip+port 上的订单服务。 在微服务架构中,服务之间是存在依赖的。例如在订单系统中创建订单时,需要对用户信息做快照,这时候也就意味着这个流程要依赖: 订单、用户两个系统。当前大型网站的语境下,多服务分布式共存,单个服务也可能会跑在多台物理/虚拟机上。所以即使你知道你需要依赖的是“订单服务”这个具体的服务,实际面对的仍然是多个 ip+port 组成的集群。因此你需要: 1. 通过“订单服务”这个名字找到它对应的 ip+port 列表2. 决定把这个请求发到哪一个 ip+port 上的订单服务。
ip+port 的组合往往被称为 endpoint。通过“订单服务”去找到这些 endpoint 的过程,叫做服务发现。选择把请求发送给哪一台机器,以最大化利用下游机器的过程,叫做负载均衡。本节主要讨论服务发现。 ip+port 的组合往往被称为 endpoint。通过“订单服务”去找到这些 endpoint 的过程,叫做服务发现。选择把请求发送给哪一台机器,以最大化利用下游机器的过程,叫做负载均衡。本节主要讨论服务发现。
## 为什么不用 ip+port 直接连依赖服务? ## 为什么不把 ip+port 写死在自己的配置文件中
TODO 从系统的演化角度来讲,加图
在大多数公司发展初期,物理机器比较少,内网 ip 也很少。一些创业公司虽然开发人员众多,但因为业务限制,每一个服务的 QPS 都不高。因此确实有很多公司服务之间是通过 ip+port 来进行相互调用的。再原始一些的话,甚至可能所有服务都在一个工程下,那也就没有什么依赖问题了。 在大多数公司发展初期,物理机器比较少,内网 ip 也很少。一些创业公司虽然开发人员众多,但因为业务限制,每一个服务的 QPS 都不高。因此确实有很多公司服务之间是通过 ip+port 来进行相互调用的。再原始一些的话,甚至可能所有服务都在一个工程下,那也就没有什么依赖问题了。
@ -58,11 +60,83 @@ redis-cli> sadd order_service.http 100.10.100.11:1002
1. 客户端主动的故障摘除 1. 客户端主动的故障摘除
2. 客户端被动故障摘除。 2. 客户端被动故障摘除。
主动的故障摘除是指,我作为依赖其它人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。 主动的故障摘除是指,我作为依赖其它人的上游,在下游一台机器挂掉的时候,我可以自己主动把它从依赖的节点列表里摘掉。常见的手段也有两种,一种是靠应用层心跳,还有一种靠请求投票。下面是一种根据请求时是否出错,对相应的服务节点进行投票的一个例子:
```go ```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 nodejava 技术栈的服务发现框架很多是基于此来做故障服务节点摘除。 被动故障摘除,最早的解决方案是 zookeeper 的 ephemeral nodejava 技术栈的服务发现框架很多是基于此来做故障服务节点摘除。
比如我们是电商的平台部的订单系统,那么可以建立类似这样的永久节点:
```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。

View File

@ -1,4 +1,4 @@
# 6.12. Load-Balance 负载均衡 # 5.11. Load-Balance 负载均衡
本节将会讨论常见的 web 后端服务之间的负载均衡手段。 本节将会讨论常见的 web 后端服务之间的负载均衡手段。
@ -178,3 +178,7 @@ map[6:143275 5:143054 3:143584 2:143031 1:141898 0:142631 4:142527]
``` ```
分布结果和我们推导出的理论是一致的。 分布结果和我们推导出的理论是一致的。
## 基于一致性哈希的负载均衡
## ketama hash

View File

@ -0,0 +1 @@
# 5.12. Dist-config 分布式配置服务

View File

@ -0,0 +1 @@
# 5.13. Circuit-Breaker 熔断保护

View File

@ -0,0 +1 @@
# 5.8. Monitor metrics 和服务监控

View File

@ -1,4 +1,4 @@
# 第章 go 和 web # 第章 go 和 web
本章将会阐述 go 在 web 开发方面的现状,并以几个典型的开源 web 框架为例,带大家深入 web 框架本身的执行流程。 本章将会阐述 go 在 web 开发方面的现状,并以几个典型的开源 web 框架为例,带大家深入 web 框架本身的执行流程。

View File

@ -0,0 +1,3 @@
# 6.1. 云上地鼠
TODO

3
ch6-cloud/ch6-02-raft.md Normal file
View File

@ -0,0 +1,3 @@
# 6.2. Raft协议
TODO

3
ch6-cloud/ch6-03-hash.md Normal file
View File

@ -0,0 +1,3 @@
# 6.3. 分布式哈希
TODO

View File

@ -0,0 +1,3 @@
# 6.4. 分布式队列
TODO

View File

@ -0,0 +1,3 @@
# 6.5. 分布式缓存
TODO

3
ch6-cloud/ch6-06-etcd.md Normal file
View File

@ -0,0 +1,3 @@
# 6.6. etcd
TODO

View File

@ -0,0 +1,3 @@
# 6.7. confd
TODO

3
ch6-cloud/ch6-08-lock.md Normal file
View File

@ -0,0 +1,3 @@
# 6.8. 分布式锁
TODO

View File

@ -0,0 +1,3 @@
# 6.9. 分布式任务调度系统
TODO

View File

@ -0,0 +1,3 @@
# 6.10. 延时任务系统
TODO

3
ch6-cloud/ch6-11-k8s.md Normal file
View File

@ -0,0 +1,3 @@
# 6.11. Kubernetes
TODO

3
ch6-cloud/ch6-12-faq.md Normal file
View File

@ -0,0 +1,3 @@
# 6.12. 补充说明
TODO

4
ch6-cloud/readme.md Normal file
View File

@ -0,0 +1,4 @@
# 第六章 分布式系统
Go语言号称是互联网时代的C语言。现在的互联网系统已经不是以前的一个主机搞定一切的时代互联网时代的服务后台有大量的分布式系统构成任何单一后台服务器节点的故障并不会导致整个系统的停机。同时以青云、阿里云、腾讯云为代表的云厂商崛起标志着云时代的到来在云时代分布式编程将成为一个基本技能。而基于Go语言构建的Docker、K8s等系统正是推动了云时代的提前到来。本章将简单讨论如何使用Go语言开发各种分布式系统。

View File

@ -1 +0,0 @@
# 6.7. Ratelimit 服务流量限制

View File

@ -1 +0,0 @@
# 6.8. Dist-config 分布式配置服务

View File

@ -1 +0,0 @@
# 6.9. Circuit-Breaker 熔断保护

View File

@ -1 +0,0 @@
# 6.8. Monitor metrics 和服务监控

3
ch7-ast/readme.md Normal file
View File

@ -0,0 +1,3 @@
# 第七章 Go 和 AST
AST是抽象语法树的缩写abstract syntax tree一般可以用一个树型结构表示源代码的抽象语法结构。比如一个算术表达式可以用AST表示if分支结构、for循环结构也可以用AST表示。因为树是一个任意分叉的AST也可以非常容易if分支、for循环等嵌套的结构。了解AST不仅仅可以加深对语言本身的理解基于AST也可以做很多有意义的事情比如分析某类型的BUG、进行某种优化等。更让人兴奋的是Go语言标准库已经内置了强大易用的AST库让我们了解一下这种神秘的技术吧。

View File

@ -1,4 +1,4 @@
# 第七章 go 和那些生产力工具 # 第八章 Go和那些生产力工具
在日常开发中我们难免遇到很多重复劳动,程序员的天性使他们更倾向于消灭重复劳动。哪怕花半小时去写脚本,也一定要消灭五分钟的痛苦。这样才能让生活更美好。 在日常开发中我们难免遇到很多重复劳动,程序员的天性使他们更倾向于消灭重复劳动。哪怕花半小时去写脚本,也一定要消灭五分钟的痛苦。这样才能让生活更美好。