1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 12:32:21 +00:00

ch1-4: 图片增加标题

This commit is contained in:
chai2010 2018-08-14 22:08:04 +08:00
parent ffab977819
commit fe008c4f7d
7 changed files with 55 additions and 0 deletions

View File

@ -380,6 +380,9 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X
![](../images/ch2.3-1-x-ptr-to-y-ptr.uml.png) ![](../images/ch2.3-1-x-ptr-to-y-ptr.uml.png)
*图 2.3-1 X类型指针转Y类型指针*
任何类型的指针都可以通过强制转换为`unsafe.Pointer`指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。 任何类型的指针都可以通过强制转换为`unsafe.Pointer`指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。
## 2.3.6 数值和指针的转换 ## 2.3.6 数值和指针的转换
@ -392,6 +395,9 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X
![](../images/ch2.3-2-int32-to-char-ptr.uml.png) ![](../images/ch2.3-2-int32-to-char-ptr.uml.png)
*图 2.3-2 int32和`char*`指针转换*
转换分为几个阶段在每个阶段实现一个小目标首先是int32到uintptr类型然后是uintptr到`unsafe.Pointr`指针类型,最后是`unsafe.Pointr`指针类型到`*C.char`类型。 转换分为几个阶段在每个阶段实现一个小目标首先是int32到uintptr类型然后是uintptr到`unsafe.Pointr`指针类型,最后是`unsafe.Pointr`指针类型到`*C.char`类型。
## 2.3.7 切片间的转换 ## 2.3.7 切片间的转换
@ -418,4 +424,7 @@ pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
![](../images/ch2.3-3-x-slice-to-y-slice.uml.png) ![](../images/ch2.3-3-x-slice-to-y-slice.uml.png)
*图 2.3-3 X类型切片转Y类型切片*
针对CGO中常用的功能作者封装了 "github.com/chai2010/cgo" 包,提供基本的转换功能,具体的细节可以参考实现代码。 针对CGO中常用的功能作者封装了 "github.com/chai2010/cgo" 包,提供基本的转换功能,具体的细节可以参考实现代码。

View File

@ -10,6 +10,9 @@
![](../images/ch2.5-1-cgo-generated-files.dot.png) ![](../images/ch2.5-1-cgo-generated-files.dot.png)
*图 2.5-1 cgo生成的中间文件*
包中有4个Go文件其中nocgo开头的文件中没有`import "C"`指令其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 `_cgo_gotypes.go` Go文件其中包含Go语言部分辅助代码。此外还会创建一个 `_cgo_export.h``_cgo_export.c` 文件对应Go语言导出到C语言的类型和函数。 包中有4个Go文件其中nocgo开头的文件中没有`import "C"`指令其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 `_cgo_gotypes.go` Go文件其中包含Go语言部分辅助代码。此外还会创建一个 `_cgo_export.h``_cgo_export.c` 文件对应Go语言导出到C语言的类型和函数。
## 2.5.2 Go调用C函数 ## 2.5.2 Go调用C函数
@ -134,6 +137,8 @@ void _cgo_506f45f9fa85_Cfunc_sum(void *v) {
![](../images/ch2.5-2-call-c-sum-v1.uml.png) ![](../images/ch2.5-2-call-c-sum-v1.uml.png)
*图 2.5-2 调用C函数*
其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。 其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。
## 2.5.3 C调用Go函数 ## 2.5.3 C调用Go函数
@ -245,5 +250,7 @@ func runtime.cgocallback(fn, frame unsafe.Pointer, framesize, ctxt uintptr)
![](../images/ch2.5-3-call-c-sum-v2.uml.png) ![](../images/ch2.5-3-call-c-sum-v2.uml.png)
*图 2.5-3 调用导出的Go函数*
其中`runtime.cgocallback`函数是实现C语言到Go语言函数跨界调用的关键。更详细的细节可以参考相关函数的实现。 其中`runtime.cgocallback`函数是实现C语言到Go语言函数跨界调用的关键。更详细的细节可以参考相关函数的实现。

View File

@ -67,6 +67,9 @@
![](../images/ch3.2-1-arch-hsm-zero.jpg) ![](../images/ch3.2-1-arch-hsm-zero.jpg)
*图 3.2-1 人力资源机器*
整个程序只有一个输入指令、一个输出指令和两个跳转指令共四个指令: 整个程序只有一个输入指令、一个输出指令和两个跳转指令共四个指令:
``` ```
@ -88,6 +91,9 @@ X86其实是是80X86的简称后面三个字母包括Intel 8086、80286
![](../images/ch3.2-2-arch-amd64-01.ditaa.png) ![](../images/ch3.2-2-arch-amd64-01.ditaa.png)
*图 3.2-2 AMD64架构*
左边是内存部分是常见的内存布局。其中text一般对应代码段用于存储要执行指令数据代码段一般是只读的。然后是rodata和data数据段数据段一般用于存放全局的数据其中rodata是只读的数据段。而heap段则用于管理动态的数据stack段用于管理每个函数调用时相关的数据。在汇编语言中一般重点关注text代码段和data数据段因此Go汇编语言中专门提供了对应TEXT和DATA命令用于定义代码和数据。 左边是内存部分是常见的内存布局。其中text一般对应代码段用于存储要执行指令数据代码段一般是只读的。然后是rodata和data数据段数据段一般用于存放全局的数据其中rodata是只读的数据段。而heap段则用于管理动态的数据stack段用于管理每个函数调用时相关的数据。在汇编语言中一般重点关注text代码段和data数据段因此Go汇编语言中专门提供了对应TEXT和DATA命令用于定义代码和数据。
中间是X86提供的寄存器。寄存器是CPU中最重要的资源每个要处理的内存数据原则上需要先放到寄存器中才能由CPU处理同时寄存器中处理完的结果需要再存入内存。X86中除了状态寄存器FLAGS和指令寄存器IP两个特殊的寄存器外还有AX、BX、CX、DX、SI、DI、BP、SP几个通用寄存器。在X86-64中又增加了八个以R8-R15方式命名的通用寄存器。因为历史的原因R0-R7并不是通用寄存器它们只是X87开始引入的MMX指令专有的寄存器。在通用寄存器中BP和SP是两个比较特殊的寄存器其中BP用于记录当前函数帧的开始位置和函数调用相关的指令会隐式地影响SP的值SP则对应当前栈指针的位置和栈相关的指令会隐式地影响SP的值而某些调试工具需要BP寄存器才能正常工作。 中间是X86提供的寄存器。寄存器是CPU中最重要的资源每个要处理的内存数据原则上需要先放到寄存器中才能由CPU处理同时寄存器中处理完的结果需要再存入内存。X86中除了状态寄存器FLAGS和指令寄存器IP两个特殊的寄存器外还有AX、BX、CX、DX、SI、DI、BP、SP几个通用寄存器。在X86-64中又增加了八个以R8-R15方式命名的通用寄存器。因为历史的原因R0-R7并不是通用寄存器它们只是X87开始引入的MMX指令专有的寄存器。在通用寄存器中BP和SP是两个比较特殊的寄存器其中BP用于记录当前函数帧的开始位置和函数调用相关的指令会隐式地影响SP的值SP则对应当前栈指针的位置和栈相关的指令会隐式地影响SP的值而某些调试工具需要BP寄存器才能正常工作。
@ -103,6 +109,9 @@ Go汇编为了简化汇编代码的编写引入了PC、FP、SP、SB四个伪
![](../images/ch3.2-3-arch-amd64-02.ditaa.png) ![](../images/ch3.2-3-arch-amd64-02.ditaa.png)
*图 3.2-3 Go汇编的伪寄存器*
在AMD64环境伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部不包括参数和返回值部分一般用于定位局部变量。伪SP是一个比较特殊的寄存器因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部一般用于定位调用其它函数的参数和返回值。 在AMD64环境伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部不包括参数和返回值部分一般用于定位局部变量。伪SP是一个比较特殊的寄存器因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部一般用于定位调用其它函数的参数和返回值。
当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)``+8(SP)`没有标识符前缀为真SP寄存器`a(SP)``b+8(SP)`有标识符为前缀表示伪寄存器。 当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)``+8(SP)`没有标识符前缀为真SP寄存器`a(SP)``b+8(SP)`有标识符为前缀表示伪寄存器。

View File

@ -112,6 +112,9 @@ DATA ·num+8(SB)/8,$0
![](../images/ch3.3-1-pkg-var-decl-01.ditaa.png) ![](../images/ch3.3-1-pkg-var-decl-01.ditaa.png)
*图 3.3-1 变量定义*
汇编代码中并不需要NOPTR标志因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。 汇编代码中并不需要NOPTR标志因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。
@ -173,6 +176,9 @@ IEEE754标准中最高位1bit为符号位然后是指数位指数为采
![](../images/ch3.3-2-ieee754.jpg) ![](../images/ch3.3-2-ieee754.jpg)
*图 3.3-2 IEEE754浮点数结构*
IEEE754浮点数还有一些奇妙的特性比如有正负两个0除了无穷大和无穷小Inf还有非数NaN同时如果两个浮点数有序那么对应的有符号整数也是有序的反之则不一定成立因为浮点数中存在的非数是不可排序的。浮点数是程序中最难琢磨的角落因为程序中很多手写的浮点数字面值常量根本无法精确表达浮点数计算涉及到的误差舍入方式可能也的随机的。 IEEE754浮点数还有一些奇妙的特性比如有正负两个0除了无穷大和无穷小Inf还有非数NaN同时如果两个浮点数有序那么对应的有符号整数也是有序的反之则不一定成立因为浮点数中存在的非数是不可排序的。浮点数是程序中最难琢磨的角落因为程序中很多手写的浮点数字面值常量根本无法精确表达浮点数计算涉及到的误差舍入方式可能也的随机的。
下面是在Go语言中声明两个浮点数如果没有在汇编中定义变量那么声明的同时也会定义变量 下面是在Go语言中声明两个浮点数如果没有在汇编中定义变量那么声明的同时也会定义变量
@ -302,12 +308,18 @@ func makechan(chanType *byte, size int) (hchan chan any)
![](../images/ch3.3-3-pkg-var-decl-02.ditaa.png) ![](../images/ch3.3-3-pkg-var-decl-02.ditaa.png)
*图 3.3-3 变量定义*
变量在data段分配空间数组的元素地址依次从低向高排列。 变量在data段分配空间数组的元素地址依次从低向高排列。
然后再查看下标准库图像包中`image.Point`结构体类型变量的内存布局: 然后再查看下标准库图像包中`image.Point`结构体类型变量的内存布局:
![](../images/ch3.3-4-pkg-var-decl-03.ditaa.png) ![](../images/ch3.3-4-pkg-var-decl-03.ditaa.png)
*图 3.3-4 结构体变量定义*
变量也时在data段分配空间变量结构体成员的地址也是依次从低向高排列。 变量也时在data段分配空间变量结构体成员的地址也是依次从低向高排列。
因此`[2]int``image.Point`类型底层有着近似相同的内存布局。 因此`[2]int``image.Point`类型底层有着近似相同的内存布局。

View File

@ -40,6 +40,9 @@ TEXT ·Swap(SB), NOSPLIT, $0
![](../images/ch3.4-1-func-decl-01.ditaa.png) ![](../images/ch3.4-1-func-decl-01.ditaa.png)
*图 3.4-1 函数定义*
第一种是最完整的写法函数名部分包含了当前包的路径同时指明了函数的参数大小为32个字节对应参数和返回值的4个int类型。第二种写法则比较简洁省略了当前包的路径和参数的大小。如果有NOSPLIT标注会禁止汇编器为汇编函数插入栈分裂的代码。NOSPLIT对应Go语言中的`//go:nosplit`注释。 第一种是最完整的写法函数名部分包含了当前包的路径同时指明了函数的参数大小为32个字节对应参数和返回值的4个int类型。第二种写法则比较简洁省略了当前包的路径和参数的大小。如果有NOSPLIT标注会禁止汇编器为汇编函数插入栈分裂的代码。NOSPLIT对应Go语言中的`//go:nosplit`注释。
目前可能遇到的函数标志有NOSPLIT、WRAPPER和NEEDCTXT几个。其中NOSPLIT不会生成或包含栈分裂代码这一般用于没有任何其它函数调用的叶子函数这样可以适当提高性能。WRAPPER标志则表示这个是一个包装函数在panic或runtime.caller等某些处理函数帧的地方不会增加函数帧计数。最后的NEEDCTXT表示需要一个上下文参数一般用于闭包函数。 目前可能遇到的函数标志有NOSPLIT、WRAPPER和NEEDCTXT几个。其中NOSPLIT不会生成或包含栈分裂代码这一般用于没有任何其它函数调用的叶子函数这样可以适当提高性能。WRAPPER标志则表示这个是一个包装函数在panic或runtime.caller等某些处理函数帧的地方不会增加函数帧计数。最后的NEEDCTXT表示需要一个上下文参数一般用于闭包函数。
@ -81,6 +84,7 @@ TEXT ·Swap(SB), $0-32
![](../images/ch3.4-2-func-decl-02.ditaa.png) ![](../images/ch3.4-2-func-decl-02.ditaa.png)
*图 3.4-2 函数定义*
下面的代码演示了如何在汇编函数中使用参数和返回值: 下面的代码演示了如何在汇编函数中使用参数和返回值:
@ -138,6 +142,8 @@ Foo函数的参数和返回值的大小和内存布局
![](../images/ch3.4-3-func-arg-01.ditaa.png) ![](../images/ch3.4-3-func-arg-01.ditaa.png)
*图 3.4-3 函数的参数*
下面的代码演示了Foo汇编函数参数和返回值的定位 下面的代码演示了Foo汇编函数参数和返回值的定位
@ -210,6 +216,9 @@ func Foo() {
![](../images/ch3.4-4-func-local-var-01.ditaa.png) ![](../images/ch3.4-4-func-local-var-01.ditaa.png)
*图 3.4-4 函数的局部变量*
从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的FP寄存器对应第一个参数的开始地址第一个参数地址较低因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的而伪SP寄存器对应的是第一个局部变量的结束地址第一个局部变量地址较大因此每个局部变量的便宜量都是负数。 从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的FP寄存器对应第一个参数的开始地址第一个参数地址较低因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的而伪SP寄存器对应的是第一个局部变量的结束地址第一个局部变量地址较大因此每个局部变量的便宜量都是负数。
## 3.4.5 调用其它函数 ## 3.4.5 调用其它函数
@ -241,6 +250,9 @@ func sum(a, b int) int {
![](../images/ch3.4-5-func-call-frame-01.ditaa.png) ![](../images/ch3.4-5-func-call-frame-01.ditaa.png)
*图 3.4-5 函数帧*
为了便于理解我们对真实的内存布局进行了简化。要记住的是调用函数时被调用函数的参数和返回值内存空间都必须由调用者提供。因此函数的局部变量和为调用其它函数准备的栈空间总和就确定了函数帧的大小。调用其它函数前调用方要选择保存相关寄存器到栈中并在调用函数返回后选择要恢复的寄存器进行保存。最终通过CALL指令调用函数的过程和调用我们熟悉的调用println函数输出的过程类似。 为了便于理解我们对真实的内存布局进行了简化。要记住的是调用函数时被调用函数的参数和返回值内存空间都必须由调用者提供。因此函数的局部变量和为调用其它函数准备的栈空间总和就确定了函数帧的大小。调用其它函数前调用方要选择保存相关寄存器到栈中并在调用函数返回后选择要恢复的寄存器进行保存。最终通过CALL指令调用函数的过程和调用我们熟悉的调用println函数输出的过程类似。
Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用参数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。 Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用参数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。

View File

@ -11,6 +11,9 @@
![](../images/ch3.6-1-func-stack-frame-layout-01.ditaa.png) ![](../images/ch3.6-1-func-stack-frame-layout-01.ditaa.png)
*图 3.6-1 函数调用参数布局*
首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后汇编器自动插入了BP寄存器相关的指令因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时先从栈恢复BP和SP寄存器接着取出的返回地址跳转到对应的指令执行。 首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后汇编器自动插入了BP寄存器相关的指令因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时先从栈恢复BP和SP寄存器接着取出的返回地址跳转到对应的指令执行。

View File

@ -167,6 +167,9 @@ grpc-gateway的工作原理如下图
![](../images/ch4.6-1-grpc-gateway.png) ![](../images/ch4.6-1-grpc-gateway.png)
*图 4.6-1 Grpc-Gateway工作流程*
通过在Protobuf文件中添加路由相关的元信息通过自定义的代码插件生成路由相关的处理代码最终将REST请求转给更后端的GRPC服务处理。 通过在Protobuf文件中添加路由相关的元信息通过自定义的代码插件生成路由相关的处理代码最终将REST请求转给更后端的GRPC服务处理。
路由扩展元信息也是通过Protobuf的元数据扩展用法提供 路由扩展元信息也是通过Protobuf的元数据扩展用法提供