❝「第12期」 距离大叔的80期小目标还有68期,搁了一段时间的大叔又回来了~ 今天大叔要跟大家分享基础知识点是 —— defer坑坑洼洼的使用细节。还是那句话,基础抓得狠,不愁没饭碗,最怕的就是面试官的面试记录是这样写:“该同学基础较差,暂不考虑”,扎不扎心,难不难过。所以,建议提倡鼓励倡导表扬大家跟着大叔一起踏踏实实地打好基础,一起进步吧!
❞
文章开始前,想请大家先看一下关于defer用法的两道题,据说这是5年的Gopher都会掉进的坑哦:
func increaseA() int {
var i int
defer func() {
i++
}()
return i
}
func increaseB() (r int) {
defer func() {
r++
}()
return r
}
func main() {
fmt.Println(increaseA())
fmt.Println(increaseB())
}
老实说,在不运行代码的前提下,大叔未能够完全回答上来。如果你的情况也跟大叔一样,或者对defer的知识点有点模凌两可,那么大叔这篇文章就有意义了,请接着往下看;如果你能正确回答上了(跪烂的膝盖还收吗大神),那么就当做温故知新吧。
好,在分析上面题目之前,我们先来了解一下defer的今生前世是什么。
defer是什么
defer 是 Go 语言提供的一种用于注册延迟调用的机制,或者你也可以认为 defer 是一个“延迟调用函数”。我们看一下 defer 关键字在 Go 语言源代码中对应的数据结构:
type _defer struct {
siz int32 // 参数的长度,函数fn的参数长度
started bool // 该 defer 是否已经执行过
openDefer bool // 是否开发编码
sp uintptr // 函数栈指针寄存器,一般指向当前函数栈的栈顶
pc uintptr // 程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC 指向的是下一条指令,而不是当前指令
fn *funcval // 指向传入的函数地址和参数
_panic *_panic // 指向_panic链表
link *_defer // 指向_defer链表
...
}
可以看到,runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。
那么这个链表是怎么构建的呢?实际上,只要获取到 新的runtime._defer 结构体,它都会被追加到所在 Goroutine _defer链表的最前面,即往 _defer链表的表头追加。
什么时候执行?如何执行?
defer 既然被定义为 “延迟调用”,说明 defer 语句不会立即执行,而是跟上面提到的那样,程序获取到新的 defer 结构体时,会先往延迟调用链表的表头追加该defer结构体。
那么什么时候会执行延迟调用呢?答案是:在函数return前。
在函数return前,当前的 Goroutine 会从表头开始遍历延迟调用链表依次执行每个defer(也就是说最先被定义的defer语句会被最后执行,没错,就是先进后出的执行顺序,所以该延迟调用链也可以理解为是一个栈),我们来看一下源码:
func deferreturn(arg0 uintptr) {
gp := getg() // 返回当前的goroutine
d := gp._defer // 获取g上绑定的第一个defer
if d == nil { // 由于是递归调用,这里是一个循环终止条件,d上已经没有绑定的defer了
return
}
sp := getcallersp() // 获取当前调用者的sp
if d.sp != sp {
// 判断当前调用者栈是否和defer中保存的一致
// 举个例子,a()中声明一个defer1,并调用b(),b中也声明一个defer2
// 然后defer1和defer2都绑定在同一个g上
// 那么在b()执行return时,只会执行defer2,因为defer2上绑定的才是b()的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
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link // g中的defer指向下一个defer
freedefer(d) // 进行释放,归还到相应的缓冲区或者让gc回收
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // // 执行defer中绑定的func
}
整个执行流程可以理解为:
- 判断当前goroutine上是否还有绑定的defer,若没有,直接return;
- 获取 goroutine 绑定的 defer 链表头部的defer;
- 判断当前 defer 中存储的sp是否和调用者的sp一致,若不一致,也直接return,证明当前defer不是在此调用函数中声明的;
- 进行参数的拷贝;
- 释放当前要执行的fn关联的defer;
- 执行 jmpdefer 函数,这里执行完fn的逻辑后会递归调用 deferreturn 函数。
坑坑洼洼的使用细节
上面整体介绍了defer 数据结构以及调用原理,接下来我们继续看一下 defer 的使用细节。
一号坑:变量引用
在使用 defer 时,不可避免地会涉及到变量引用的问题,实际上,defer 语句定义时对外部变量引用的方式有以下两种:
作为函数参数
外部变量作为函数参数时,在 defer 定义时就把值传递给 defer 函数体缓存起来,相当于将变量值复制一份(注意:如果参数是值类型,那么将复制值,如果参数是指针,那么将复制指针而不是复制指针指向的值),看个例子:
func main() {
i := 0
// 作为函数参数
defer func(x int) {
fmt.Printf("复制值:%v\n", x)
}(i)
defer func(x *int) {
fmt.Printf("复制指针:%v\n", *x)
}(&i)
i++
}
运行输出:
复制指针:1
复制值:0
作为闭包引用
外部变量作为闭包引用时,则会在 defer 函数体真正被调用是根据整个上下文确定当前的值。看例子:
func main() {
i := 0
// 作为闭包引用
defer func() {
fmt.Println(i) // 打印 1
}()
i++
}
二号坑:和返回值被命名的函数一起使用
使用 defer 最容易踩坑的地方是和带命名返回参数的函数一起使用,这种方式又称为函数显式返回。比如这样:
func test() (r int) {
defer func() {
// todo
}()
return 0
}
避免掉坑的关键是要正确理解下面这条语句:
return xxxx
没错,就是这个return语句,实际上 return xxxx 并不是一个原子指令,经过编译后,它会变成三条指令(return “三步曲”):
返回值 = xxxx
调用 defer 函数体
直接 return , 结束当前函数
第一、三步才是 return 语句的真正命令,第二步是 defer 定义的语句,这里有可能会操作返回值。
实践是检验真理的唯一标准!结合上面的知识点我们来盘几道题吧。
func f1() (r int) {
defer func() {
r++
}()
return 0
}
func main() {
fmt.Println(f1())
}
首先,我们可以看到,defer 定义时对外部变量引用的方式是 闭包引用的方式,既然是闭包引用的方式,那么 defer 函数体内对返回值操作将会影响返回值;接着我们根据 return 的“三步曲”对上面的代码进行拆解:
func f1() (r int) {
// 1.赋值
r = 0
// 2.闭包引用,返回值被修改
defer func() {
r++
}()
// 3.直接 return
return
}
func main() {
fmt.Println(f1())
}
经过拆解后,整个操作已经非常清晰了,defer 是闭包引用,返回值被修改,所以函数 f1() 返回 1。
再来:
func f2() (r int) {
t := 1
defer func() {
t = t + 5
}()
return t
}
func main() {
fmt.Println(f2())
}
同样,先看 defer 定义时对外部变量引用的方式,明显也是闭包引用;接着根据 return 的“三步曲”对上面的代码进行拆解:
func f2() (r int) {
t := 1
// 1.赋值
r = t
// 2.闭包引用,但是没有修改返回值 r,修改的是变量 t
defer func() {
t = t + 5
}()
// 3.直接 return
return
}
func main() {
fmt.Println(f2())
}
通过上面的拆解分析,我们可以看到,即使 defer 是闭包引用,但是 defer 函数体内修改的是变量 t,跟返回值 r 没有一毛钱关系,所以函数 f2() 返回 1。
趁热打铁,继续搞:
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
func main() {
fmt.Println(f3())
}
直接来吧,“三步曲” 拆起来:
func f3() (r int) {
// 1.赋值
r = 1
// 2.r 作为函数参数,不会修改要返回的那个 r 值
defer func(r int) {
r = r + 5
}(r)
// 3.直接 return
return
}
func main() {
fmt.Println(f3())
}
这里注意一下,在第二步中,r 作为函数参数使用,是值的复制,因此 defer 函数体内的 r 和 外部的 r 是两个变量,defer 函数体内变量 r 的改变不会影响 外部变量 r ,所以函数 f3() 的返回值应该是 1。
问:如果和匿名返回值的函数使用呢?
上面的例子我们讨论的是函数显示返回值方式(带命名返回参数),那如果函数匿名返回值呢?我们再看看文章开头的例子:
func increaseA() int {
var i int
defer func() {
i++
}()
return i
}
我们可以看到 函数increaseA() 是匿名返回值,直接返回局部变量,同时 defer 函数对局部变量是闭包引用,defer 函数也会操作这个局部变量。
同样的,我们依然是根据 return “三步曲” 走,对于匿名返回值,我们可以假设有一个变量存储返回值,假设返回值变量为 dashu,那么上面代码是不是可以拆解为:
dashu = i
i++
return
所以当程序编译 return i 时,首先会把 变量 i 的值拷贝给 变量 dashu,尽管 defer 是闭包引用的方式,并且在 defer 函数中修改了变量 i 的值,但对返回值 dashu 不造成影响,所以最终函数 increaseA() 返回0。
三号坑:defer 函数参数含有函数
最后一个细节,如果defer的函数的参数中又以某个函数的返回值为当作参数,那么 defer 又是怎么表现的呢?可以参考大叔公众号文章:传送门
好了,以上就是关于 defer 使用细节的全部内容。有收获的小伙伴点个赞呗,3Q~
关注公众号「大叔说码」 跟大叔一起打基础,我们下期见~