理解Golang中的defer
关键字defer用于实现延迟调用,根据Golang官方的定义:A defer statement defers the execution of a function until the surrounding function returns. 。但是,当返回值与defer相互关联时,如果没有正确理解defer与return真正的执行顺序,那么容易出现一些不可描述的现象。
我们先运行如下代码,根据运行结果来理解defer,在查看运行结果之前,不妨先想想main函数的输出是什么。
package main
import "fmt"
func main() {
fmt.Println(funcA())
fmt.Println(funcB())
}
func funcA() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return i
}
func funcB() (i int) {
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return i
}
运行结果如下。
defer1: 1
defer2: 2
0
defer1: 1
defer2: 2
2
根据运行结果,我们可以看到,defer语句的执行顺序以及打印出来的变量i的值是意料之内的,区别在于函数的返回值,而funcA和funcB这两个函数唯一的区别则是函数的返回值有没有被命名,因此导致两个函数返回值不同的原因也应该和返回值是否命名有关。
在计算机科学中,我相信很多原理性的东西都是可以相互解释的。在本篇文章中,我决定用Java中的try-catch-finally来解释defer的运行机制。首先,先看看如下Java代码片段,并考虑返回值有哪几种情况。
public int func() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
如果我们对Java熟悉,那么应该知道,无论在try块中是否出现异常,finally块中的语句是一定要执行的,但是函数的返回值只可能是1或者2,绝对不会是3。我们通过查看这段Java代码对应的字节码指令来理解这一点。
public int func();
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
这段字节码对应于try-finally这条执行轨迹:iconst_1将常量1压入操作数栈;istore_1将栈顶元素弹出并存储于局部变量表Slot1处;iload_1将局部变量表Slot1处的元素压入操作数栈;istore_2将操作数栈顶元素弹出并存储于局部变量表Slot2处;iconst_3将常量3压入操作数栈;istore_1将操作数栈顶元素弹出并存储于局部变量表Slot1处;iload_2将局部变量表Slot2处的元素压入操作数栈;ireturn返回栈顶元素。
第0条指令和第1条指令实现了x = 1,并且x的值存储于局部变量表的Slot1处;第2条指令和第3条指令将x的值拷贝了一份,并存储在局部变量表的Slot2处。第4条指令和第5条指令实现了x = 3;第6条指令和第7条指令将存储于局部变量表Slot2处的x的拷贝值返回,由于Slot2中的值是执行finally块中语句之前x的值,因此返回值等于2。
基于以上解释,我们再来重新理解defer。对于函数funcA,当执行至return时,变量i的值等于0,与Java类似,Golang会将返回值的拷贝值(即变量i的值)存储于内存中的某个位置pos(对应于局部变量表的Slot2处),然后执行defer语句,当defer语句执行完后,尽管变量i的值已增加至2,但是返回值依赖于地址pos处的值,因此funcA返回0;对于函数funcB,由于funcB已经命名了函数的返回值为变量i,这意味着函数的返回值的地址即为变量i的地址。当执行至return时,尽管变量i的值为0,但是紧接着的defer语句使得变量i的值增加至2,由于funcA的返回值的地址为变量i的地址,因此funcB最后的返回值为2。
为了验证我们的解释,运行如下代码。
func main() {
fmt.Println(*(funcC()))
}
func funcC() *int {
var i int
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return &i
}
尽管函数funcC的返回值并没有提前声明,但是funcC的返回值仍为2,这是因为funcC的返回值是变量i的内存地址,当执行到return语句时,变量i的内存地址值的拷贝会被存储于内存的某个位置,而该位置的值即是最后的返回值,由于变量i的地址在整个过程并未被修改,因此通过地址值的拷贝值我们依旧可以观察到defer语句对变量i的操作。