Go数据结构与算法05-栈下: 深入理解 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: 延迟处理,资源清理

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

基本用法 2: 后进先出

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

	fmt.Println("3")
}

基本用法 3: 异常恢复

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

容易掉坑 1: 闭包变量

// 容易掉坑之,函数变量修改
func f3() (res int) {
	defer func() {
		res++
	}()
	return 0
}

容易掉坑 2: 参数传递

// 容易掉坑之,参数复制
func f4() (res int) {
	defer func(res int) {
		res++
	}(res)
	return 0
}

源码剖析

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

// ....
 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 定义

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

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 不在堆上

d.heap = false

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

//   d.link = gp._defer
//   gp._defer = d

deferreturn

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

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

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

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

d := callDefer
if n.Esc == EscNever {
    d = callDeferStack
}
s.call(n.Left, d)

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

func f6() {
	for i := 0; i < 10; i++ {
		defer func() {
			fmt.Printf("f6: %d\n", i)
		}()
	}
}

看一下汇编结果

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 方法,被分配到栈上了