5. 栈下: 深入理解 defer

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

  • Go 数据结构与算法系列文章,本系列文章主要会包括常见的数据结构与算法实现,同时会包括 Go 标准库代码的分析理解,讲到对应章节的时候优先学习分析 Go 的源码实现,例如 slice、list、sort 等,然后可能会有一些常见的案例实现,同时这也是 极客时间-数据结构与算法之美 的课程笔记
  • 本文代码仓库: https://github.com/mohuishou/go-algorithm 🌟🌟🌟🌟🌟
  • **RoadMap: **持续更新中,预计一周更新 1 ~ 2 篇文章,预计到 202101 月底前更新完成
  • 获取更新: Github知乎RSS开发者头条
  • 上一个系列刚刚完成了 Go 设计模式,如果感兴趣也可以进行查看

深入理解 go defer

上篇文章中我们讲到栈的时候说到先入后出这种特性,在 Go 中第一时间想到的就是 defer  接下来我们就深入理解一下 defer

用法

下面先回顾一下基本的用法以及较为常见的坑,文末会给出输出结果,可以先想想会输出什么

基本用法 1: 延迟处理,资源清理

1
2
3
4
5
// 基本用法:延迟调用,清理资源
func f0() {
defer fmt.Println("clean")
fmt.Println("hello")
}

基本用法 2: 后进先出

1
2
3
4
5
6
7
// 基本用法1: 后进先出
func f1() {
defer fmt.Println("1")
defer fmt.Println("2")

fmt.Println("3")
}

基本用法 3: 异常恢复

1
2
3
4
5
6
7
8
9
// 基本用法2:异常恢复
func f2() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("paniced: %+v \n", err)
}
}()
panic("test")
}

容易掉坑 1: 闭包变量

1
2
3
4
5
6
7
// 容易掉坑之,函数变量修改
func f3() (res int) {
defer func() {
res++
}()
return 0
}

容易掉坑 2: 参数传递

1
2
3
4
5
6
7
// 容易掉坑之,参数复制
func f4() (res int) {
defer func(res int) {
res++
}(res)
return 0
}

源码剖析

想要看源码,我们需要先找到源码的位置,我们可以直接执行 go tool compile -N -l -S main.go 获取汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ....
0x00d8 00216 (main_2.go:6) PCDATA $1, $0
0x00d8 00216 (main_2.go:6) CALL runtime.deferprocStack(SB)
0x00dd 00221 (main_2.go:6) NOP
0x00e0 00224 (main_2.go:6) TESTL AX, AX
0x00e2 00226 (main_2.go:6) JNE 252
0x00e4 00228 (main_2.go:6) JMP 230
0x00e6 00230 (main_2.go:7) XCHGL AX, AX
0x00e7 00231 (main_2.go:7) CALL runtime.deferreturn(SB)
0x00ec 00236 (main_2.go:7) MOVQ 216(SP), BP
0x00f4 00244 (main_2.go:7) ADDQ $224, SP
0x00fb 00251 (main_2.go:7) RET
0x00fc 00252 (main_2.go:6) XCHGL AX, AX
0x00fd 00253 (main_2.go:6) NOP
0x0100 00256 (main_2.go:6) CALL runtime.deferreturn(SB)

我们可以看到主要是调用了 runtime.deferprocStackruntime.deferreturn  这两个运行时的方法

defer 定义

1
2
3
4
5
6
7
8
9
10
11
12
type _defer struct {
siz int32 // 所有传入参数和返回值的总大小
started bool // defer 是否执行了
heap bool // 是否在堆上,这是 go1.13 新加的,划重点
sp uintptr // 函数栈指针寄存器,一般指向当前函数栈的栈顶
pc uintptr // 程序计数器,指向下一条需要执行的指令
fn *funcval // 指向传入的函数地址和参数
_panic *_panic // 指向 panic 链表
link *_defer // 指向 defer 链表

//...
}

deferprocStack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func deferprocStack(d *_defer) {
gp := getg() // 获取 g,判断是否在用户栈上
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer // 这两个是将当前 defer 插入到链表头部,也就是defer为什么时候先入后出的原因
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}

注意这几行
说明这个 defer 不在堆上

1
d.heap = false

这两个是将当前 defer 插入到链表头部,也就是 defer 为什么时候先入后出的原因

1
2
//   d.link = gp._defer
// gp._defer = d

deferreturn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}

switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

如果函数中存在 defer 编译器就会自动在函数的最后插入一个 deferreturn

  • 清空 defer 的调用信息
  • freedefer 将 defer 对象放入到 defer 池中,后面可以复用
  • 如果存在延迟函数就会调用 runtime·jmpdefer 方法跳转到对应的方法上去
  • runtime·jmpdefer 方法会递归调用 deferreturn 一直执行到结束为止

deferproc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}

// the arguments of fn are in a perilous state. The stack map
// for deferproc does not describe them. So we can't let garbage
// collection or stack copying trigger until we've copied them out
// to somewhere safe. The memmove below does that.
// Until the copy completes, we can only call nosplit routines.
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()

d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}

// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}

除了 deferprocStack 还有 deferproc 这个方法,那这个方法和之前的方法有什么区别呢?
主要的区别就是这个方法将 defer 分配在了堆上,看下方的 newdefer

1
2
3
4
5
func newdefer(siz int32) *_defer {
// ...
d.heap = true
return d
}

其他和 deferprocStack 类似这里就不赘述了

什么时候 defer 会在堆上什么时候会在栈上?

那问题来了如何判断 defer 在堆上还是在栈上呢?
https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/escape.go#L743

1
2
3
4
5
6
topLevelDefer := where != nil && where.Op == ODEFER && e.loopDepth == 1
if topLevelDefer {
// force stack allocation of defer record, unless
// open-coded defers are used (see ssa.go)
where.Esc = EscNever
}

https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/cmd/compile/internal/gc/ssa.go#L1116

1
2
3
4
5
d := callDefer
if n.Esc == EscNever {
d = callDeferStack
}
s.call(n.Left, d)

可以看到主要是在逃逸分析的时候,发现 e.loopDepth == 1  并且不是 open-coded defer 就会分配到栈上。
这也是为什么 go 1.13 之后 defer 性能提升的原因,所以切记不要在循环中使用 defer 不然优化也享受不到
我们来验证一下

1
2
3
4
5
6
7
func f6() {
for i := 0; i < 10; i++ {
defer func() {
fmt.Printf("f6: %d\n", i)
}()
}
}

看一下汇编结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x0073 00115 (main.go:67)	CALL	runtime.deferproc(SB)
0x0078 00120 (main.go:67) TESTL AX, AX
0x007a 00122 (main.go:67) JNE 151
0x007c 00124 (main.go:67) JMP 126
0x007e 00126 (main.go:66) PCDATA $1, $-1
0x007e 00126 (main.go:66) NOP
0x0080 00128 (main.go:66) JMP 130
0x0082 00130 (main.go:66) MOVQ "".&i+32(SP), AX
0x0087 00135 (main.go:66) MOVQ (AX), AX
0x008a 00138 (main.go:66) MOVQ "".&i+32(SP), CX
0x008f 00143 (main.go:66) INCQ AX
0x0092 00146 (main.go:66) MOVQ AX, (CX)
0x0095 00149 (main.go:66) JMP 68
0x0097 00151 (main.go:67) PCDATA $1, $0
0x0097 00151 (main.go:67) XCHGL AX, AX
0x0098 00152 (main.go:67) CALL runtime.deferreturn(SB)

可以发现在循环嵌套的场景下,的确调用的是 runtime.deferproc  方法,被分配到栈上了

关注我获取更新

wechat
知乎
github

猜你喜欢