Go语言的defer是一个很方便的机制,能够把某些函数调用推迟到当前函数返回前才实际执行。我们可以很方便的用defer关闭一个打开的文件、释放一个Redis连接,或者解锁一个Mutex。而且Go语言在设计上保证,即使发生panic,所有的defer调用也能够被执行。不过多个defer函数是按照定义顺序倒序执行的。 我们在公众号有一篇文章:
【Golang】脱胎换骨的defermp.weixin.qq.com内容有点儿多,篇幅有点儿长,所以在这里我们打算拆分成四篇文章,每一篇专注一两个主要问题,可能会好消化些吧~
(一)“ defer如何延迟,因何倒序?”
(二)“ defer函数怎样传参?”
(三)“ defer+闭包,再多套几层,你还hold住吗?”
(四)“ 都说defer1.12性能有坑,那坑从何来?又该怎么填?”
func f1() {defer A()// code to do something
}
像这样一段代码,在Go1.12中编译后的伪指令是这样的(源码结合反编译整理出的伪代码,帮助理解~_~):
func f1() {r := runtime.deferproc(0, A) // 经过recover返回时r为1,否则为0if r > 0 {goto ret}// code to do somethingruntime.deferreturn()return
ret:runtime.deferreturn()
}
其中与defer指令相关的有两个部分。第一部分是deferproc,它负责保存要执行的函数信息,我们称之为defer“注册”。
func deferproc(siz int32, fn *funcval)
从函数原型来看,deferproc函数有两个参数,第一个是被注册的defer函数的参数加返回值共占多少字节;第二个参数是一个runtime.funcval结构体的指针,也就是一个Function Value。对Function Value感兴趣,可以看看这个:
网页链接mp.weixin.qq.com与defer指令相关的第二部分就是deferreturn,它被编译器插入到函数返回以前调用,负责执行已经注册的defer函数。所以defer函数之所以能延迟到函数返回前执行,就是因为先注册,后调用。
再来看看defer函数为什么会倒序执行。defer注册信息会保存到defer链表。每个goroutine在运行时都对应一个runtime.g结构体,其中有一个_defer字段,保存的就是defer链表的头指针。
deferproc新注册的defr信息会添加到链表头。deferreturn执行时也从链表头开始,所以defer才会表现为倒序执行。
理解了这些,就可以继续细化,看看defer注册时保存了什么信息,defer链表中每个元素究竟是什么结构了。
type _defer struct {siz int32started boolsp uintptr // sp at time of deferpc uintptrfn *funcval_panic *_panic // panic that is running deferlink *_defer}
siz:由deferproc第一个参数传入,就是defer函数参数加返回值的总大小。这段空间会直接分配在_defer结构体后面,用于在注册时保存给defer函数传入的参数,并在执行时直接拷贝到defer函数的调用者栈上。
started :标识defer函数是否已经开始执行;
sp:就是注册defer函数的函数栈指针;
pc:是deferproc函数返回后要继续执行的指令地址;
fn:由deferproc的第二个参数传入,也就是被注册的defer函数;
_panic:是触发defer函数执行的panic指针,正常流程执行defer时它就是nil;
link:自然是链到之前注册的那个_defer结构体。
网页链接mp.weixin.qq.com