写在前面的话:
在接触defer之后,觉得Go的这一特性很好,有点类似于C 的析构函数,不过它们却有很大的不同。主要的区别点是defer实现在函数里面,作用域也是在函数里面,当函数的return语句被调用之后,才会调用到这个defer声明的函数。而析构函数实现在类里面,作用域是在类内部,在该类的实例被销毁的时候,就会被调用到。
在谈论defer之前,笔者问了自己三个问题: 为什么我们需要defer? 如何才能更好的使用它? defer是如何实现的?
基于上面的三个问题,笔者做了简单的整理。
一.为什么我们需要defer
我们在写程序的时候,往往会碰到下面的两种情况。
第一种释放资源,当我们在创建一个资源的时候,往往需要释放资源,但是因为逻辑分支太多的缘故,我们要在每一个异常分支里面去实现释放资源的 操作。这样以来的话,就存在两个问题,第一,我们需要散弹式修改,释放资源的地方很多,每个都要填写上面,代码不容易维护。第二,异常分支太多的话,很容易漏掉,或者提前return了,进而导致资源没有释放掉,这样会产生代码漏洞。
第二种处理异常,代码实现里面,有一些异常是可以从逻辑代码里面控制的,有一些却未必容易控制,特别是一些很难捕捉到到异常,这种主要来源于操作系统内核或者硬件提示的异常信息。
1.C 里面这两种情况,都有对应的处理方法,第一种采用析构函数去释放这些资源,第二种情况采用try-catch的方式去捕获和处理这些异常(备注:这部分内容会专门整理一篇文章介绍)。
2.到了Go之后,我发现C 的这两种实现方式都不存在了,那怎么办呢?于是defer产生了,这种在普通函数的return之后会调用的延迟调用函数,该发挥作用了。
二.defer的使用规则
defer函数调用时间,发生在该函数的return之后,主要用三种使用规则,说是三种规则,其实更像是三种注意事项。
1)当defer被声明时,其参数就会被实时解析。
代码语言:javascript复制package main
import (
"fmt"
)
func main() {
var i int = 1
// i的值在defer第一次走到的位置就被确认下来了
defer fmt.Println("defer i value:", i)
i
fmt.Println("Main i value:", i)
}
output:
代码语言:javascript复制Main i value: 2
defer i value: 1
备注:对于指针来说,这个参数是地址,指针指向的数据还是有可能会被更改的。
2)当一个函数中有多个defer函数时,它们的执行顺序是先进后出。
这种处理场景,一般是有几个资源,而这些资源之间是有依赖关系的。
代码语言:javascript复制package main
import (
"fmt"
)
func main() {
var i int = 1
defer fmt.Println("defer i value:", i) // 3rd called
i
defer fmt.Println("defer i value:", i) // 2nd called
i
defer fmt.Println("defer i value:", i) // 1st called
i
fmt.Println("Main i value:", i)
}
OutPut:
代码语言:javascript复制Main i value: 4
defer i value: 3
defer i value: 2
defer i value: 1
3)defer可以读取有名返回值。
关于这一个规则,笔者觉得这是defer的一个副作用,毕竟返回值在return之后,是不希望被改掉的。不过也有好处,就是一旦希望对函数的返回值做一些特殊操作的时候,例如希望将返回值占内存很大的内容写到文件里或者内存里。
代码语言:javascript复制package main
import (
"fmt"
)
func deferFunc() (i int ) {
return 1 // 返回值1
}
func deferFunc0() (i int ) {
defer func() { i }() // 会更改i的值,但是没有办法影响返回值
return 1 // 返回值 直接将1这种数字写回了栈中,并直接返回了
}
func deferFunc1() (i int ) {
defer func() { i }()// step2: 会继续操作i
return i // step1: 返回值会复制给一个临时变量,再返回出去
}
func main() {
i := 0
i = deferFunc()
fmt.Println("Main i value:", i)
i = deferFunc0()
fmt.Println("Main0 i value:", i)
i = deferFunc1()
fmt.Println("Main1 i value:", i)
i
fmt.Println("Main2 i value:", i)
}
Output:
代码语言:javascript复制Main i value: 1
Main0 i value: 2
Main1 i value: 1
Main2 i value: 2
三.defer的实现原理
1)defer 的数据结构
代码语言:javascript复制type _defer struct{
spuintptr//函数栈指针
pcuintptr//程序计数器
fn*funcval//函数地址
link*_defer//指向自身结构的指针,用于链接多个defer
}
图片发自简书App
每次声明一个defer函数都会从链表头部开始插入。
函数返回前执行defer是从链表首部一次取出执行。
2)defer的创建与执行
deferproc():在声明defer处调用,将其defer函数存入goroutine的链表中。
deferreturn():在ret指令前调用,将defer从对应的链表中取出并执行。
过程如下:在编译阶段声明defer处插入函数deferproc() ,在函数return前插入函数deferreturn()。
参考资料:Go语言程序设计
https://studygolang.com/articles/16067
灰子作于二零一九年三月十九日。