defer的使用
基本介绍
defer
关键字用于延迟(defer)函数的执行,即使在函数执行过程中出现错误,也能保证被延迟的函数会被执行。defer
语句会把函数调用延迟到所在函数执行完成之后再执行,但是 defer
语句会在 defer
语句被定义时对参数进行求值。
defer底层原理
defer
的实现基于栈(stack)的概念。当一个函数中有多个 defer
语句时,这些 defer
语句会按照后进先出(LIFO)的顺序被压入一个栈中。在函数执行结束时,这些 defer
语句会按照相反的顺序从栈中弹出,并执行对应的函数调用。
底层原理可以简单描述如下:
- 当遇到
defer
语句时,Go 会将该语句后面的函数调用包装成一个延迟函数(deferred function)对象,并将该对象压入当前函数的延迟函数栈中。 - 在函数执行完毕之前,所有的延迟函数对象都会留在栈中不动。
- 当函数执行完毕时,Go 会按照后进先出的顺序从延迟函数栈中弹出延迟函数对象,并执行其中的函数调用。
这种机制保证了即使在函数执行过程中发生错误,也能够保证延迟函数被执行。同时,由于延迟函数是在函数定义时被注册的,而不是在函数调用时,所以它们能够访问到在函数定义时可见的变量和参数。
使用示例
示例1
这段代码的输出结果是什么呢?
|
|
由于defer的底层使用的是栈(先进后出),而且入栈时,会把函数的参数一并保存起来,所以整个程序的执行顺序如下:
- 将延迟函数 fmt.Println(a) 加入栈中,此时 a=10,一并保存在栈中;
- 将延迟函数 fmt.Println(a) 加入栈中,此时 a=20,一并保存在栈中;
- 将 a 修改为 100,并输出 a 的值,输出 100;
- 从栈中弹出一个延迟函数 fmt.Println(a),这是后加入的,a 的值为 20,输出 20;
- 再从栈中弹出一个延迟函数 fmt.Println(a),这是先加入的,a 的值为 10,输出 10;
所以整个程序的输出结果为:
|
|
示例2
这段代码的输出结果是什么呢?
|
|
在本程序中,第二个 defer 语句在 return 之后(语句条件成立),这个 defer 语句还会执行吗?
需要注意的是,在函数中可能存在多个 return
语句,每一个 return
都会导致延迟函数的执行。
所以在这个程序中,第二个 defer 并不会执行,因为在执行到第二个 defer 之前,函数就已经返回了。
在我们写程序时,一定要把 defer 语句写在函数的最前面,放在出现 return 在 defer 之前,导致 defer 无法执行的情况。
这段代码的输出结果为:
|
|
延迟调用的面试题
说明下面代码的输出结果。
|
|
本题主要考察 defer 延迟调用。
在 main 函数中:
-
定义了一个 int 类型的变量 x;
-
再定义了一个 *int 类型的指针 p,它指向了 x 所在的地址
-
然后 defer 语句,将 MakeFoo(p).Bar(p) 压入延迟函数栈中。
在 defer 语句中,先调用了 MakeFoo(p) 函数。
MakeFoo() 函数先打印了传入参数指针位置的值,也就是 p 指向位置的值 1,然后返回 Foo 类型的空结构体。
注意这里调用 MakeFoo() 函数中的 print 语句会立即执行,所以会先输出 1。
然后再用 Foo 类型的空结构体对象调用函数 Bar(p),此时不会理解执行,而是将 p 和 Bar(p) 一起压入到延迟函数栈中。(保存现场)
-
main 函数继续执行,将 x 的值改为 2;
-
使用 new(int) 创建了一个 *int 类型的指针,并赋值给 p,p 指向的位置值为 int 类型的零值 0。
new(int)
的作用是创建一个类型为int
的变量,并返回一个指向该变量的指针。这个指针指向的内存空间被初始化为int
类型的零值,即0
。 -
执行 MakeFoo(p) 函数,打印 p 指向位置的值,输出 0;
-
main 函数执行完毕,即将退出,退出之前执行压入到延迟函数栈中的函数 Bar(p),由于栈中保存的是之前 p 的值(指向 x 所在的地址),所以调用 Bar(p) 函数,输出 p 指针指向的值为 x 的值,也就是修改之后的 2。
-
所以,整个程序的输出结果为:
102
。