go defer

最近看到一道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的输入就可以理解了。

posted @ 2020-10-15 21:23  技术-刘腾飞  阅读(103)  评论(0编辑  收藏  举报