defer语句用于延迟函数的执行,每次defer都会把一个函数压入栈中,函数返回之前再把延迟的函数取出并执行。

不过延迟的函数可能有输入参数,可能来自创建defer的函数,延迟函数使用这些参数可能会影响主函数的一些行为,所以了解defer工作机制是有必要的。

defer的规则

1.延迟函数的参数在defer语句出现时就已经确定

看下面的例子

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

在defer出现的时候i是0,所以虽然后面执行了i++,并且延迟函数在a函数返回前执行,打印的结果仍然是0。

不过如果参数是一个指针的话,地址仍保持不变,但是指针指向的值可能在延迟函数执行之前又被修改了,这种修改延迟函数是能够看得到的。

2.延迟函数按照后进先出顺序执行,即先出现的defer最后执行

定义defer类似于入栈操作,执行defer类似于出栈操作,所以很好理解。

defer的设计初衷就是用来在函数返回时清理资源,资源往往有以来顺序,后申请的资源依赖于先申请的资源,释放的时候当前要反向进行,所以defer要也是如此。

3.延迟函数可能操作主函数的具名返回值

这种情况只要了解了函数的返回的原理就很容易理解了

func deferFuncResult() (result int) {
	i := 1
	defer func() {
		result++
	}()
	return i
}

可以看到上面的返回语句return i。不过return这个语句并不是一个原子操作,实际上上面的return操作可以分为两步,将i值存入栈中作为返回值然后执行跳转

result = i
return

而延迟函数的执行时机就在这两条语句中间,所以加入defer语句后的执行过程如下:

result = i
result++
return

当主函数有具名返回值的时候,延迟函数可以通过名称来操作这个返回值,从而影响主函数的返回值。如果主函数没有具名返回值的话,延迟函数没有能够访问到返回值的途径,自然也就对返回值没有影响了。

defer实现原理

src/runtime/runtime2.go:defer中定义了defer的数据结构:

type _defer struct {
	sp uintptr //函数栈指针
	pc uintptr //程序计数器
	fn *funcval //函数地址
	link *_defer //指向自身结构的指针,用于连接多个defer
}

defer后一定要接一个函数,所以defer的数据结构与一般的函数类似,有栈地址,程序计数器,函数地址等。

不过区别在于,它还有一个指针可以指向另一个defer,实际上这是一个链表,而每声明一个defer就在链表的头部插入一个defer,每执行一个defer就从链表的头部取出一个执行。

头插法,使得defer按照先进后出的顺序进行执行。

每个goroutine都已一个defer指针,在源码包src/runtime/anic.go定义了创建defer和执行defer的两个方法:

  • deferproc():在声明defer处调用,就是将defer函数存入到goroutine的defer链表中。
  • deferreturn():在return指令,准确地说是ret指令前调用,将defer从goroutine链表中取出并执行。

总结

  • defer定义地延迟函数参数在defer语句声明处就确定下来了。
  • defer定义顺序与实际执行顺序相反。
  • return不是原子操作,执行过程是:保存返回值(若有) 执行defer(若有) 执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯。