逃逸分析
1# -m 用于输出编译器的优化细节 (包含逃逸分析)
2go build -gcflags '-m -l' main.go
3
4# 看汇编 如runtime.newobject(SB)
5go tool compile -S main.go
6
7# 禁止内联和优化
8go test -bench . -benchmem -gcflag "-N -l"
查看中间件代码
1GOSSAFUNC=func_name go build main.go
defer 的底层原理
思路
- 协程记录defer信息,函数退出时调用
- 将defer代码直接编译进函数尾
思路1 堆上分配
1.12之前使用
在堆上开辟一个sched.deferpool ,p上
遇到defer语句,将信息放入deferpool
函数返回时,从deferpool取出执行
1.13之后出现
思路2 栈上分配
- 1.13之后出现
- 遇到defer语句,将信息放入栈上
- 函数返回时,从栈中取出执行
- 只能保存一个defer信息 (多个的时候还是在堆上)
思路三 开放编码
1.14之后出现
如果defer语句在编译时,就编译时就可以固定
直接改写用户代码,defer语句放入函数末尾
defer源码
1package _defer
2
3// 源码位置:$GOPATH/src/runtime/runtime2.go
4type _defer struct {
5 // 参数和返回值的内存大小
6 siz int32
7
8 //表示该_defer语句是否已经开始执行
9 started bool
10
11 //表示该_defer语句的优先级
12 //当一个_defer语句被执行时,它会被添加到_defer链表中,而heap字段则用于将_defer语句添加到一个优先队列中,
13 //以便在函数返回时按照一定的顺序执行_defer语句。在_defer链表中,后添加的_defer语句会先被执行,而在优先队列中,
14 //heap值较小的_defer语句会先被执行。这个字段的值是在_defer语句被添加到_defer链表时根据一定规则计算出来的,
15 //通常是根据_defer语句的执行顺序和作用域等因素计算而得。在函数返回时,Go语言会按照heap值的大小顺序执行_defer语句。
16 //如果多个_defer语句的heap值相同,则它们会按照它们在_defer链表中的顺序依次执行。
17 //这个机制可以确保_defer语句按照一定的顺序执行,从而避免了一些潜在的问题。
18 heap bool
19
20 // 表示该_defer用于具有开放式编码_defer的帧。开放式编码_defer是指在编译时已经确定_defer语句的数量和位置,
21 //而不是在运行时动态添加_defer语句。在一个帧中,可能会有多个_defer语句,但只会有一个_defer结构体记录了所有_defer语句的信息,
22 //而openDefer就是用来标识该_defer结构体是否是针对开放式编码_defer的
23 openDefer bool
24
25 //_defer语句所在栈帧的栈指针(stack pointer)
26 //在函数调用时,每个函数都会创建一个新的栈帧,用于保存函数的局部变量、参数和返回值等信息。
27 //而_defer语句也被保存在这个栈帧中,因此需要记录栈指针以便在函数返回时找到_defer语句。
28 //当一个_defer语句被执行时,它会被添加到_defer链表中,并记录当前栈帧的栈指针。
29 //在函数返回时,Go语言会遍历_defer链表,并执行其中的_defer语句。而在执行_defer语句时,
30 //需要使用保存在_defer结构体中的栈指针来访问_defer语句所在栈帧中的局部变量和参数等信息。
31 //需要注意的是,由于_defer语句是在函数返回之前执行的,因此在执行_defer语句时,函数的栈帧可能已经被销毁了。
32 //因此,_sp字段的值不能直接使用,需要通过一些额外的处理来确保_defer语句能够正确地访问栈帧中的信息。
33 sp uintptr
34
35 //_defer语句的程序计数器(program counter)
36 //程序计数器是一个指针,指向正在执行的函数中的下一条指令。在_defer语句被执行时,它会被添加到_defer链表中,
37 //并记录当前函数的程序计数器。当函数返回时,Go语言会遍历_defer链表,并执行其中的_defer语句。
38 //而在执行_defer语句时,需要让程序计数器指向_defer语句中的函数调用,以便正确地执行_defer语句中的代码。
39 //这就是为什么_defer语句需要记录程序计数器的原因。需要注意的是,由于_defer语句是在函数返回之前执行的,
40 //因此在执行_defer语句时,程序计数器可能已经指向了其它的函数或代码块。因此,在执行_defer语句时,
41 //需要使用保存在_defer结构体中的程序计数器来确保_defer语句中的代码能够正确地执行。
42 pc uintptr // pc 计数器值,程序计数器
43
44 // defer 传入的函数地址,也就是延后执行的函数
45 fn *funcval
46
47 //defer 的 panic 结构体
48 _panic *_panic
49
50 //用于将多个defer链接起来,形成一个defer栈
51 //当程序执行到一个 defer 语句时,会将该 defer 语句封装成一个 _defer 结构体,并将其插入到 defer 栈的顶部。
52 //当函数返回时,程序会从 defer 栈的顶部开始依次执行每个 defer 语句,直到 defer 栈为空为止。
53 //每个 _defer 结构体中的 link 字段指向下一个 _defer 结构体,从而将多个 _defer 结构体链接在一起。
54 //当程序执行完一个 defer 语句后,会将该 defer 从 defer 栈中弹出,并将其 link 字段指向的下一个 _defer 结构体设置为当前的 defer 栈顶。
55 //这样,当函数返回时,程序会依次执行每个 defer 语句,从而实现 defer 语句的反转执行顺序的效果。
56 //需要注意的是,由于 _defer 结构体是在运行时动态创建的,因此 defer 栈的大小是不固定的。
57 //在编写程序时,应该避免在单个函数中使用大量的 defer 语句,以免导致 defer 栈溢出。
58 link *_defer
59}
60
61
62
63func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
64 gp := getg() //获取goroutine结构
65 if gp.m.curg != gp {
66 // go code on the system stack can't defer
67 throw("defer on system stack")
68 }
69 ...
70 d := newdefer(siz) //新建一个defer结构
71 if d._panic != nil {
72 throw("deferproc: d.panic != nil after newdefer")
73 }
74 d.link = gp._defer // 新建defer的link指针指向g的defer
75 gp._defer = d // 新建defer放到g的defer位置,完成插入链表表头操作
76 d.fn = fn
77 d.pc = callerpc
78 d.sp = sp
79 ...
80}
实际应用
-
释放锁
-
捕获异常 panic
-
取消上下文 defer cancel()
-
关闭资源, 文件具柄,数据库关闭等
如何关闭10万个文件 ?
处理单个文件利用 defer close 文件具柄, 利用waitgroup控制10万个go程能执行完成.
revocer 如何拯救 panic
-
panic会抛出错误
-
终止协程运行
-
如果没recover,带崩整个go程序
-
recover 必须在defer申请的匿名函数中执行
-
recover和函数调用在同一个协程才能捕捉当前函数的panic
panic 会执行本协程已经注册的defer语句,上级协程的defer语句不会执行
panic->runtime.gopanic() 会看有没有设置recover
如果设置了recover, defer会使用堆上分配(deferpool)
遇到panic,panic会从deferpool取出defer语句,执行
defer中调用recover,可以终止panic的过程。
cgo 原理
让go语言调用c方法
- 在内存中开辟一个结构体
- 结构体中含有参数和返回值
- 结构体地址传入c方法
- c方法将讲过写入返回值的位置
cgo需要调度器的配合
- 协程需要抢占式调度 (不断切换 ,morestack强制切换)
- 进入c程序之后,调度器无法抢占协程
- 调度器停止对此协程的调度
协程栈的切换
- c的栈不受runtime管理
- 进入c时,需要将当前栈切换到线程的系统栈上
cgo优缺点
- cgo可以让go调用现成的c实现 (最大用途 ,不想用go重写)
- cgo限制了go语言的跨平台特性
- cgo并不能提高go语言的性能