前言
如果你用过 defer ,那么你会大概知道 defer 是用于延迟函数的调用。每次调用 defer 都会将一个函数压入栈中,执行的时候从栈顶取出函数来执行,那么它底层的原理到底是怎么样的?今天我们一起来探究一下。
defer 的使用规则
延迟函数的参数在defer语句出现时就已经确定下来了
func test() {
num := 0
defer fmt.Println(num)
num = 3
return
}
输出:0
在定义 defer fmt.Println(num) 的时候,参数就已经复制了一份,传递过去了,所以输出的结果为 0
延迟函数执行按后进先出顺序执行,即先出现的 defer最后执行
即是后进先出,类似于进栈出栈,如果你接着往下看,就会了解到,它底层就是维护了一个单链表。新建 defer 的时候,就压入单链表的头部,调用的时候,就从头部取出一个 defer,这样就有了先进后出的效果
延迟函数可能操作主函数的具名返回值(面试常考)
怎么理解这句话呢?
定义一个函数,如果这个函数的返回值是匿名的,那么 defer 不会影响到这个匿名返回值
定义一个函数,如果这个函数的返回值是具名的,就是我给这个返回值定义了变量名,那么 defer 可能会影响到这个 具名返回值。
有朋友说,你光说这么两句话,我也不太理解这是什么意思呀?别急,本文的最后一个小节,我会举例说明一下。
defer 底层数据结构
go/src/runtime/runtime2.go
defer 的结构体如下,
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
openDefer bool
sp uintptr // stack point 栈指针
pc uintptr // process count 程序计数器
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // 指向下一个 defer,因为这是一个单链表
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr
framepc uintptr
}
goroutine 的结构体如下,比较主要的字段,我都加了注释。可以看出,这里是有一个专门的字段用来保存 defer 的,它的底层是一个单链表。每次定义一个 defer 都会将 defer 插入到单链表头部,每次执行时,都从单链表头部获取 一个 defer,这样就有后进先出的执行效果。
type g struct {
stack stack // 协程的 栈,
sched gobuf // 在协程切换的时候,用于保存上下文
goid int64 //goroutine 的ID
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
_defer *_defer // defer 指针,指向一个 defer 单链表,每次定义一个 defer 都会插入到单链表头部,每次执行时,都从单链表头部获取 一个 defer,这样就有后进先出的执行效果。
此处省略很多参数
defer 指针,指向一个 defer 单链表,每次定义一个 defer 都会插入到单链表头部,每次执行都从从不获取,这样就有后进先出的执行效果。
那么,基于上面的解释,我们来画图理解一下:
defer 的创建和执行
源码在
/usr/local/go/src/runtime/panic.go
我们可以找到 deferproc() 和 deferreturn() 这两个函数,这两个方法分别用于创建和执行 defer。
deferproc() 做了什么事情呢?
- 获取 deferproc之前的 sp 寄存器的值
- 创建一个 _defer 结构体,然后把这个 _defer 放到 goroutine 的 *_defer 字段,并且将他放到当前的 _defer 链表的头
- 初始化 _defer 结构体的参数
- return0
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
gp := getg() //获取一个 goroutine
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
if goexperiment.RegabiDefer && siz != 0 {
// TODO: Make deferproc just take a func().
throw("defer with non-empty frame")
}
sp := getcallersp() //获取当前寄存器的值
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz) //创建 defer 结构体实例
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d //给 goroutine 的单链表赋值
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))
}
return0()
}
deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。
可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。
- 获取 g、获取 p
- 执行 defer
- 运行结束后释放 defer
func deferreturn() {
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
}
argp := getcallersp() + sys.MinFrameSize
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(argp)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(argp), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
_ = fn.fn
jmpdefer(fn, argp)
}
defer 对于不同情况的处理
匿名返回值
func test() (int) {
t := 2
defer func() {
t = 10
}()
return t
}
func main() {
fmt.Println(test())
}
输出 2
具名返回值
func test() (t int) { //t初始化0, 并且作用域为该函数全域
defer func() {
t = t * 10
}()
return 1
}
func main() {
fmt.Println(test())
}
输出 10
发生 panic
func main() {
defer_call()
fmt.Println("main 正常结束")
}
func defer_call() {
defer func() { fmt.Println("defer: panic 之前1") }()
defer func() { fmt.Println("defer: panic 之前2") }()
panic("异常内容") //触发defer出栈
defer func() { fmt.Println("defer: panic 之后,永远执行不到") }()
}
输出:
defer: panic 之前2
defer: panic 之前1
panic: 异常内容
//... 异常堆栈信息