从一个坑反看defer的使用规则

先看一段代码

package main

import (
"fmt"
)

func c() (i int) {
defer func() { i++ }()
return 1
}

func main() {
fmt.Println(c())
}

这段代码的输出结果不是应该是 1 吗?那么我们运行看看,实际结果输出是 2 !

了解golang的人都知道,在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。

因此,defer通常用来释放函数内部变量。

那么上面的例子为什么结果是 2 而不是 1 呢,我们通过defer的规则来分析到底坑在哪。

规则一 当defer被声明时,其参数就会被实时解析
还是通过代码来理解这个规则

package main

import (
"fmt"
)

func a() {
i := 0
defer fmt.Println(i)
i++
return
}

func main() {
a()
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出 1 呢?

No!最后的输出结果是 0 !

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i),但这个变量 i 在defer被声明的时候,就已经确定其确定的值了。 换言之,上面的代码等同于下面的代码:

package main

import (
"fmt"
)

func a() {
i := 0
defer fmt.Println(i) // 因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
i++
return
}

func main() {
a()
}

为了更为明确的说明这个问题,我们继续定义一个defer:

package main

import (
"fmt"
)

func a() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0
i++
defer fmt.Println(i) //输出1,因为i此时就是1
return
}

func main() {
a()
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值。

但为什么是先输出 1,在输出 0 呢? 看下面的规则二。

规则二 defer执行顺序为先进后出
还是先看一段代码:

package main

import (
"fmt"
)

func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}

func main() {
b()
}

输出的结果并不是大家设想的 0123,而是 4321。

从结果看的出,当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。

规则三 defer可以读取有名返回值
这个规则是前文提到坑之所在了!

回顾一下前面的代码

package main

import (
"fmt"
)

func c() (i int) {
defer func() {
i++
}() // (2) i++ 之后 i = 2
return 1 // (1) i = 1
}

func main() {
fmt.Println(c())
}

在开头的时候,我们说过defer是在return调用之后才执行的。

这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。

因此defer仍然可以读取c函数内的变量。

当执行 return 1 之后,i 的值就是 1。此时此刻,defer代码块开始执行,对i进行自增操作。

因此输出 2。

结合代码,我们理解了defer的三个使用规则,可帮助我们再日后使用更好的避开它的坑。