golang defer
defer是golang的一个特色功能,defer用来声明一个延迟函数,把这个函数类似于放到一个栈上,在方法return之前触发调用。我们经常用他来做一些资源的释放,比如关闭io操作。
defer的具体规则:
- 延迟函数的参数在defer语句出现时就已经确定下来了。
- 延迟函数执行按后进先出顺序执行,即先出现的defer后执行(类似于栈);
- 延迟函数可能操作主函数的具名返回值;
- 即使出现panic,defer函数也会正常执行。
解释:
- defer语句中的参数值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量的修改不会影响函数的执行。注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。
- 定义defer类似于入栈操作,执行defer类似于出栈操作。设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请资源A,再根据A资源申请B资源,再根据资源B申请资源C,即申请顺序是:A->B->C,释放时往往又要反向进行,这就是把deffer设计成FIFO的原因。每申请到一个用完需要释放的资源时,立即定义一个defer来释放资源是个很好的习惯。
- 定义defer的函数,即主函数可能有返回值,返回值没有名字没有关系,defer所作用的函数,即延迟函数可能会影响返回值。首先必须要了解:return关键字不是一个原子操作,实际上return只代表汇编指令ret,表示程序即将跳转执行。return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。这里还要分几种情况来看:
- 第一:主函数为匿名返回值,return返回的是字面量,这种情况,defer无法修改返回值;
- 第二:主函数为匿名返回值,return返回的是本地或局部变量,这种情况defer可以引用到返回值,但是不会影响返回值,因为return之前的返回值实际上是存到了栈上,然后defer只是拷贝了一份,当然如果return的是指针,情况就不一样了。defer可以修改指针指向的值。
- 第三:主函数为命名(具名)返回值,主函数中声明带名字的返回值,会初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值,如果defer语句操作该返回值,可能会改变返回结果。
- 为什么panic了defer函数也会执行,这个需要结合defer函数的实现来说了。
defer数据结构:
type _defer struct {
sp uintptr //函数栈指针
pc uintptr //程序计数器
fn *funcval //函数地址
link *_defer //指向自身结构的指针,用于链接多个defer
}
defer后面一定要接一个函数,所以defer的数据结构和一般函数类似,也有栈指针、程序计数器、函数地址等等。
与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer就从单链表表头取出一个defer执行。
defer的创建和执行:
源码包src/runtime/panic.go
定义了两个方法分别用于创建defer和执行defer:
deferproc()
: 在声明defer处调用,将其defer函数存入goroutine的链表中;deferreturn()
: 在return指令,准确的讲是在ret指令前调用,将其defer从goroutine链表中取出并执行;
可以这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。
总结:
- defer定义的延迟函数参数在defer语句定义时就已经确定下来了;
- defer定义顺序与实际执行顺序相反;
- return不是原子操作,执行过程是:保存返回值(若有)–>执行defer(若有)–>执行ret跳转;
- 申请资源后立即使用defer关闭资源是好习惯。