TrumanWong

defer容易犯错场景

TrumanWong
5/14/2023

简介

deferGo语言中的关键字,也是Go语言的重要特性之一。

defer语法形式如下:

defer Expression

其后必须跟一个函数调用或者方法调用,不能用括号括起来。在Go语言中,defer一般被用于资源的释放及异常panic的处理。

本文主要介绍defer非常容易犯错的场景:参数预计算和返回值陷阱。

参数预计算

defer的一个特性是参数的预计算,这一特性时常导致开发者在使用defer时犯错。因为在大部分时候,我们记住的都是其延迟执行的特性。参数的预计算指当函数到达defer语句时,延迟调用的参数将立即求值,传递到defer函数中的参数将预先被固定,而不会等到函数执行完成后再传递参数到defer中。

如下例所示:

func Example1() {
    a := 1
    defer func(b int) {
        fmt.Printf("defer b: %d", b)
    }(a + 1)
    a = 100
    // Output: defer b: 2
}

defer函数最终输出的b值为2。原因是传递到defer的参数是预执行的,因此在执行到defer语句时,执行了a+1并将其保留起来了,直到函数执行完成后才执行defer函数体内的语句

返回值陷阱

如下所示:

var a = 1

func f() (b int) {
    defer func() {
        a = 2
    }()
    fmt.Printf("f: a=%d, ", a)
    return a
}

func Example2() {
    i := f()
    fmt.Printf("main: i=%d, a=%d", i, a)
    // Output: f: a=1, main: i=1, a=2
}

从程序输出结果可以推测出:return之后,执行了defer函数

将上述代码稍作修改,如下所示:

var a = 1

func f() (b int) {
    b = a
    defer func() {
        b = 2
    }()
    b = 0
    return b
}

func Example() {
    i := f()
    fmt.Printf("main: i=%d, a=%d", i, a)
    // Output:  main: i=2, a=1
}

从这个结果中,我们又可以推测出defer执行完成后,执行了return语句。因为其返回值i是在defer中赋值的。

两种代码的输出结果推出的结论截然相反,原因在于return其实并不是一个原子操作,其包含了下面几步:

  1. 将返回值保存在栈上
  2. 执行defer函数
  3. 函数返回

所以第一个示例中的函数f可以翻译为如下伪代码,最终返回值b为1:

a = 1
b = a
a = 2
return

第二个示例中的函数f可以翻译为如下伪代码,最终返回值b为2

a = 1
b = a
b = 0
b = 2
return

总结

deferGo语言重要的特性之一,defer一般用于资源释放、异常捕获、中间件等场景。当有多个defer函数时,按照后入先出last-in-first-out, LIFO的顺序执行。另外,特别要注意与返回值结合时的陷阱,这是由于return语句并不是一条原子操作。