defer的使用

基本介绍

defer 关键字用于延迟(defer)函数的执行,即使在函数执行过程中出现错误,也能保证被延迟的函数会被执行。defer 语句会把函数调用延迟到所在函数执行完成之后再执行,但是 defer 语句会在 defer 语句被定义时对参数进行求值。

defer底层原理

defer 的实现基于栈(stack)的概念。当一个函数中有多个 defer 语句时,这些 defer 语句会按照后进先出(LIFO)的顺序被压入一个栈中。在函数执行结束时,这些 defer 语句会按照相反的顺序从栈中弹出,并执行对应的函数调用。

底层原理可以简单描述如下:

  1. 当遇到 defer 语句时,Go 会将该语句后面的函数调用包装成一个延迟函数(deferred function)对象,并将该对象压入当前函数的延迟函数栈中。
  2. 在函数执行完毕之前,所有的延迟函数对象都会留在栈中不动。
  3. 当函数执行完毕时,Go 会按照后进先出的顺序从延迟函数栈中弹出延迟函数对象,并执行其中的函数调用。

这种机制保证了即使在函数执行过程中发生错误,也能够保证延迟函数被执行。同时,由于延迟函数是在函数定义时被注册的,而不是在函数调用时,所以它们能够访问到在函数定义时可见的变量和参数

使用示例

示例1

这段代码的输出结果是什么呢?

1
2
3
4
5
6
7
8
func main() {
    a := 10
    defer fmt.Println(a)
    a = 20
    defer fmt.Println(a)
    a = 100
    fmt.Println(a)
}
分析

由于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;

所以整个程序的输出结果为:

1
2
3
100
20
10

示例2

这段代码的输出结果是什么呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	a := 10
	defer fmt.Println("First defer")
	if a == 10 {
		fmt.Println(a)
		return
	}
	defer fmt.Println("Second defer")
    return
}
分析

在本程序中,第二个 defer 语句在 return 之后(语句条件成立),这个 defer 语句还会执行吗?

需要注意的是,在函数中可能存在多个 return 语句,每一个 return 都会导致延迟函数的执行

所以在这个程序中,第二个 defer 并不会执行,因为在执行到第二个 defer 之前,函数就已经返回了。

在我们写程序时,一定要把 defer 语句写在函数的最前面,放在出现 return 在 defer 之前,导致 defer 无法执行的情况。

这段代码的输出结果为:

1
2
10
First defer

延迟调用的面试题

说明下面代码的输出结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Foo struct {
    v int
}
func MakeFoo(n *int) Foo {
    print(*n)
    return Foo{}
}
func (Foo) Bar(n *int) {
    print(*n)
}
func main() {
    var x = 1
    var p = &x
    defer MakeFoo(p).Bar(p)
    x = 2
    p = new(int)
    MakeFoo(p)
}
分析

本题主要考察 defer 延迟调用。

在 main 函数中:

  1. 定义了一个 int 类型的变量 x;

  2. 再定义了一个 *int 类型的指针 p,它指向了 x 所在的地址

  3. 然后 defer 语句,将 MakeFoo(p).Bar(p) 压入延迟函数栈中。

    在 defer 语句中,先调用了 MakeFoo(p) 函数。

    MakeFoo() 函数先打印了传入参数指针位置的值,也就是 p 指向位置的值 1,然后返回 Foo 类型的空结构体。

    注意这里调用 MakeFoo() 函数中的 print 语句会立即执行,所以会先输出 1。

    然后再用 Foo 类型的空结构体对象调用函数 Bar(p),此时不会理解执行,而是将 p 和 Bar(p) 一起压入到延迟函数栈中。(保存现场)

  4. main 函数继续执行,将 x 的值改为 2;

  5. 使用 new(int) 创建了一个 *int 类型的指针,并赋值给 p,p 指向的位置值为 int 类型的零值 0。

    new(int) 的作用是创建一个类型为 int 的变量,并返回一个指向该变量的指针。这个指针指向的内存空间被初始化为 int 类型的零值,即 0

  6. 执行 MakeFoo(p) 函数,打印 p 指向位置的值,输出 0;

  7. main 函数执行完毕,即将退出,退出之前执行压入到延迟函数栈中的函数 Bar(p),由于栈中保存的是之前 p 的值(指向 x 所在的地址),所以调用 Bar(p) 函数,输出 p 指针指向的值为 x 的值,也就是修改之后的 2。

  8. 所以,整个程序的输出结果为:102

0%