理解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
的操作。