逃逸分析

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语言的性能