这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
Golang关键字defer使用错误及原因 | 青训营笔记
起因
看到一位同学做大项目时提出的疑问:“我明明给statusCode赋值为4了,还是显示3 。。 defer不是最后再执行的吗”
先说结论:
runtime_deferproc函数创建新的延迟调用时会立刻拷贝函数中引用的外部参数,如果参数被闭包包裹,则拷贝的是参数的指针。- 后调用的defer函数会被追加到
_defer链表最前面,运行runtime._defer时是从前到后依次执行。因此执行以栈的形式,后入先出呈现。
参考:Go语言实现与设计
那么这个同学的问题,就是defer拷贝了c.JSON()所需的外部参数,其他同学给出了两种解决方法:
- defer放最后
- 参数用闭包包裹
传参问题
看到方案二这个闭包包裹,突然想到前几天看得一篇文章:go基础语法50问,来看看你的go基础合格了吗?。里面有一个例子当时没看懂,现在更迷糊了:
func main() {
for i := 0; i < 5; i++ {
i := i
defer func() {
fmt.Println(i)
}()
}
很明显,这是延迟执行了一个匿名函数,因此参数被闭包包裹
如果没有i:=i这一行,程序会依次输出 5 5 5 5 5
加上这一行,则会输出 4 3 2 1 0
解释这个现象,要从defer原理和GO语言函数传递方式两方面结合分析。
Golang的参数传递是值传递,而不是引用传递。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。
没有i:=i
对于这种情况, 每一轮循环中defer都拷贝了一遍i的地址。虽然i的存的常数地址在随for循环改变,但是i的地址不变。因此,main函数结束时, i存的是5的地址,defer对拷贝来的参数指针解引用,输出5.
加上i:=i
实际上每轮for循环创建了一个局部变量,为方便区分,称呼为i'。defer拷贝的是i'的地址,而不是i的。因此输出的数字不同。
实验验证
验证代码如下:
func main() {
for i := 0; i < 5; i++ {
fmt.Println("origin:", &i)
i := i
fmt.Println("partial:", &i)
defer func() {
fmt.Println("defer:", &i)
}()
}
}
输出如下:
| 循环index | origin | partial |
|---|---|---|
| 0 | 0xc0000180b8 | 0xc0000180d8 |
| 1 | 0xc0000180b8 | 0xc0000180f0 |
| 2 | 0xc0000180b8 | 0xc0000180f8 |
| 3 | 0xc0000180b8 | 0xc000018100 |
| 4 | 0xc0000180b8 | 0xc000018108 |
defer: 0xc000018108
defer: 0xc000018100
defer: 0xc0000180f8
defer: 0xc0000180f0
defer: 0xc0000180d8
可以看到for循环中的i地址一直不变,defer拷贝的是局部变量i'的地址,并且在main函数结束后以后入先出方式逆序输出。