关键字defer用于实现延迟调用,根据Golang官方的定义:A defer statement defers the execution of a function until the surrounding function returns. 。但是,当返回值与defer相互关联时,如果没有正确理解deferreturn真正的执行顺序,那么容易出现一些不可描述的现象。

我们先运行如下代码,根据运行结果来理解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的值是意料之内的,区别在于函数的返回值,而funcAfuncB这两个函数唯一的区别则是函数的返回值有没有被命名,因此导致两个函数返回值不同的原因也应该和返回值是否命名有关。

在计算机科学中,我相信很多原理性的东西都是可以相互解释的。在本篇文章中,我决定用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的操作。