defer 使用的一点点总结

64 阅读2分钟

我正在参加「掘金·启航计划」

最近在网上看到下面一个考察 defer 使用的面试题。我们都知道 defer 就是在函数返回前执行的延迟函数,常用来关闭流、通道以及断开数据库连接。我们在调用 defer 时需要分析对应使用场景和机制。

package main

import "fmt"

type temp struct{}

func (t *temp) Add(elem int) *temp {
 fmt.Println(elem)
 return &temp{}
}

func main() {
 tt := &temp{}
 defer tt.Add(1).Add(2)
 tt.Add(3)
}

首先提出两个问题:

1.defer 是什么时候调用的,其执行顺序应该是怎样的?

2.defer 是如何读取参数的?又是怎么返回参数的值的?

defer 调用机制

func a() {
	i := 0
	defer fmt.Println(i)
	i++

}

运行以上代码,控制台输出:

0

我们都知道 golang 函数是值传递的,显然在调用 defer 延迟函数时,就立刻拷贝上一行代码执行的参数,于是输出为 0,而这里实际上 defer 拷贝不是值,而是函数指针,指向的 0 的那个地址。

记住“最后一个最先出”

func deferInloop() {
	for i :=0 ; i< 5; i++ {
		defer fmt.Println(i)
	}
}

运行结果如下:

4
3
2
1
0

分析一下,下面的例子执行输出是什么?

func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x
}
func f4() (x int) {
	// ☆☆☆ x = 0
	defer func(x int) {
		x++
	}(x)
	return 5
}

func main() {
	fmt.Println(f1()) // 5
	fmt.Println(f2()) // 6
	fmt.Println(f3()) // 5
	fmt.Println(f4()) // 5
}

你分析对了吗? 这里最具迷惑的就是 f4()函数。别急,下面我们一起来分析下。

defer 调用原理

这里我们将 deferInloop() 这个函数生成汇编代码进行分析与解构。

CALL	runtime.deferproc(SB)
TESTL	AX, AX
JNE	139
JMP	28
CALL	runtime.deferreturn(SB)
MOVQ	32(SP), BP
ADDQ	$40, SP
RET

在编译器运行时 defer 延时函数分别调用 runtime 的 deferproc()deferreturn()

func deferproc(fn func())

// 创建一个新的不带参数和结果的延时函数fn;编译器会在调用defer时转换成对该函数的调用。
func deferproc(fn func()) {
	gp := getg()
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}

	d := newdefer()
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
	d.link = gp._defer
	gp._defer = d
	d.fn = fn
	d.pc = getcallerpc()
	// 我们不能在调用getcallersp和将其存储到d.sp之间被抢占,因为getcallersp的结果是一个uintptr堆栈指针。
	d.sp = getcallersp()

	// deferproc正常返回0。停止panic的延迟func使deferproc返回1。编译器生成的代码总是检查返回值,如果deferproc返回,则跳转到函数的末尾!= 0。
	return0()
}

该函数主要是创建一个 runtime._defer 结构体,赋值相关参数到对应的内容中。重点来了,这里调用了 newdefer 这个函数。下面我们先跳到对应的源码进一步分析。

func newdefer() *_defer

// 每个 P 都持有一个为 defer使用的池。
// 分配一个defer, 通常会占用per-P 池。
// 每个defer 都必须用 freedefer进行释放。
func newdefer() *_defer {
	var d *_defer
	mp := acquirem()
	pp := mp.p.ptr()
	if len(pp.deferpool) == 0 && sched.deferpool != nil {
		lock(&sched.deferlock)
		for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
			d := sched.deferpool
			sched.deferpool = d.link
			d.link = nil
			pp.deferpool = append(pp.deferpool, d)
		}
		unlock(&sched.deferlock)
	}
	if n := len(pp.deferpool); n > 0 {
		d = pp.deferpool[n-1]
		pp.deferpool[n-1] = nil
		pp.deferpool = pp.deferpool[:n-1]
	}
	releasem(mp)
	mp, pp = nil, nil

	if d == nil {
		// Allocate new defer.
		d = new(_defer)
	}
	d.heap = true
	return d
}

该函数功能就是获取 _defer 结构体,其中主要通过三种方式来取出:

1. 当调度器的 defer 缓存池`ched.deferpool`存在,则取出放到当前 Goroutine 的defer缓存池 `pp.deferpool`中;
2. 当前 Goroutine的defer缓存池 `pp.deferpool`存在,则截取n-1个前的`pp.deferpool`;
3. 上面两种情况都不存在时,直接在堆上创建一个新的_defer 结构体。

执行该方法后我们知道当前的Goroutine 的_defer 链最前面会放入得到的_defer,在执行时最后的_defer就会被最先执行。_deferreturn函数的执行过程就是取出Goroutine 上 _defer链表最前面的_defer的结构体,调用deferreturn函数,具体源码如下:

func deferreturn()

// deferreturn 函数运行延迟函数给调用方, 编译器会在defer函数末尾插入该函数进行调用。
func deferreturn() {
	gp := getg()
	for {
		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
		}

		fn := d.fn
		d.fn = nil
		gp._defer = d.link
		freedefer(d)
		fn()
	}
}

上面会判断Goroutine中 _defer链表是否都执行了_defer结构体,执行完成后则释放掉。

通过对源码的分析,我们不难分析出在文章开头的那个面试题的执行顺序。其顺序是tt.Add(1), tt.Add(3), tt.Add(2),即输出:1 3 2.