最近看到一道Go语言的面试题,如下:
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
我们来公布一下答案:
打印后
打印中
打印前
panic: 触发异常
defer
可以看出,Go语言中的defer函数是一个后进先出的机制。为什么会这个样子呢,我们先来看一下defer的实现:
//
type _defer struct {
siz int32
started bool
heap bool
openDefer bool
sp uintptr // 栈指针
pc uintptr // 调用方的程序计数器
fn *funcval // 传入的函数
_panic *_panic
link *_defer // 指向下一个执行的defer函数
fd unsafe.Pointer
varp uintptr
framepc uintptr
}
这个是defer的定义,我们主要关注link这个属性,link指向了下一个defer函数,这说明defer使用的是链表这种数据结构,那么我们再看一下defer的link是怎么建立的吧。
defer的初始化其实是发生在deferproc的newdefer方法,至于为什么,由于不是本文的重点,所以这里就不做过多的描述,可以参考golang中defer的编译调用过程:
那么我们重点看一下newdefer这个函数:
//
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg() // 获得当前的goroutine
...
d.link = gp._defer // 现在新的defer函数的link指向了当前的defer函数
gp._defer = d // 新的defer函数现在是第一个被调用的函数了
return d
}
从这里看出,每建立一个新的defer函数,都会把新defer函数的link指向之前的defer函数,同时把新defer函数作为当前goroutine第一个被调用的函数。这是一个典型的链表生成的栈。当然上面只是一个压栈的过程,defer函数并没有执行,真正执行是在deferreturn中,这是由Go语言的编译过程决定的的,具体可以参考上面的链接。
那我们再看一下deferreturn这个函数:
//
func deferreturn(arg0 uintptr) {
gp := getg() // 获得当前的goroutine
d := gp._defer
if d == nil { // 如果没有defer函数,直接return
return
}
...
fn := d.fn // 获得defer的func函数
d.fn = nil // 重置
gp._defer = d.link // 将前一个defer函数attach到当前goroutine
freedefer(d) // 释放defer函数
_ = fn.fn // 执行defer的func函数
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
这里我们可以看出,defer的调用过程是一个出栈的过程,所以一开始面试题defer的输入就可以理解了。