Golang关键字defer使用错误及原因 | 青训营笔记

133 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

Golang关键字defer使用错误及原因 | 青训营笔记

起因

看到一位同学做大项目时提出的疑问:“我明明给statusCode赋值为4了,还是显示3 。。 defer不是最后再执行的吗”

img_v2_266431d6-50e0-45b4-bdfb-55b59e0bd0bg.jpg

先说结论

  1. runtime_deferproc函数创建新的延迟调用时会立刻拷贝函数中引用的外部参数,如果参数被闭包包裹,则拷贝的是参数的指针。
  2. 后调用的defer函数会被追加到_defer链表最前面,运行runtime._defer时是从前到后依次执行。因此执行以栈的形式,后入先出呈现。

参考:Go语言实现与设计

那么这个同学的问题,就是defer拷贝了c.JSON()所需的外部参数,其他同学给出了两种解决方法:

  1. defer放最后
  2. 参数用闭包包裹

传参问题

看到方案二这个闭包包裹,突然想到前几天看得一篇文章: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. image.png

加上i:=i

实际上每轮for循环创建了一个局部变量,为方便区分,称呼为i'defer拷贝的是i'的地址,而不是i的。因此输出的数字不同。

image.png

实验验证

验证代码如下:

func main() {
   for i := 0; i < 5; i++ {
      fmt.Println("origin:", &i)
      i := i
      fmt.Println("partial:", &i)
      defer func() {
         fmt.Println("defer:", &i)
      }()
   }
}

输出如下:

循环indexoriginpartial
00xc0000180b80xc0000180d8
10xc0000180b80xc0000180f0
20xc0000180b80xc0000180f8
30xc0000180b80xc000018100
40xc0000180b80xc000018108

defer: 0xc000018108
defer: 0xc000018100
defer: 0xc0000180f8
defer: 0xc0000180f0
defer: 0xc0000180d8

可以看到for循环中的i地址一直不变,defer拷贝的是局部变量i'的地址,并且在main函数结束后以后入先出方式逆序输出。