Golang 中 defer 的用法

defer 关键字

defer 类似其他 OOP 语言中的 finally 关键字。

defer 的运作机制

defer 只能运行在函数或者方法内部,如下:

func foo() {
    defer func() {
        fmt.Println("exec defer func")
    }()
}
func(p *P)String() {
    defer func() {
        fmt.Println("exec defer func")
    }()
}

defer 关键字后面只能接函数或方法,这些函数被称为 deferred 函数(例如上面的匿名 func)。defer 将它们注册到其所在 goroutine 用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前被按后进先出(LIFO)的顺序调度执行。

defer 运转机制

直观代码展示:

func foo() {
    for i :=0; i<=3; i++ {
        defer func(n int){
            fmt.Println("n: ", n)
        }(i)
    }
}

以上代码的入栈顺序结果为:

func(0)
func(1)
func(2)
func(3)
// output:
// n: 3
// n: 2
// n: 1
// n: 0
// Last In First Out

defer 的常用方法

1.deferred func + return

当一个函数被 deferred 后,该函数中 return 的内容会被丢弃

func foo() (a, b int) {
   fmt.Println("exec foo")
   // 函数 foo 被 deferred 后 return 语句会被丢弃
   return 1, 2
}

func bar() (x, y int) {
   defer foo()
   return 1, 2
}

func TestFooBar(t *testing.T) {
   fmt.Println("test foo bar")
   x, y := bar()
   fmt.Println("x:", x, "y:", y)
}

2.defer 释放资源

defer 的一个主要用途就是释放资源,其中 mu.Lock() 与 defer mu.Unlock() 是成对出现的,这样成对出现的惯例极大降低了遗漏资源释放的可能性。

func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
    mu.Lock()
    defer mu.Unlock()
    f, err := os.OpenFile(fname, os.O_RDWR, 0666)
    if err != nil {
        return err
    }
    defer f.Close()

    _, err = f.Seek(0, 2)
    if err != nil {
        return err
    }

    _, err = f.Write(data)
    if err != nil {
        return err
    }
    return f.Sync()
}

3.defer + panic

defer 的第二个重要用途就是拦截 panic,并按需要对 panic 进行处理,可以尝试从 panic 中恢复(这也是 Go 语言中唯一的从 panic 中恢复的手段)

func genPanic() {
   fmt.Println("gen a panic")
   panic(-1)
}

func panicFunc() {
   defer func() {
      if recover() != nil {
         fmt.Println("catch panic")
      }
   }()
   genPanic()
}

func TestPanicFunc(t *testing.T) {
   fmt.Println("exec start")
   panicFunc()
   fmt.Println("exit normally")
}
// output:
//exec start
//gen a panic
//catch panic
//exit normally
//--- PASS: TestPanicFunc (0.00s)

WARNING

特别说明:deferred 函数虽然可以拦截绝大部分的 panic,但无法拦截并恢复一些运行时之外的致命问题。比如通过 C 代码“制造”的崩溃,deferred 函数便无能为力的。

4.修改具名返回值

func genReturns(i, j int) (x, y int) {
   defer func() {
      x = x * 5
      y = y * 10
   }()
   x = i + 1
   y = j + 2
   return
}

func TestReturns(t *testing.T) {
   x, y := genReturns(1, 2)
   fmt.Println("x: ", x, "y: ", y)
}

// output
//x:  10 y:  40
//--- PASS: TestReturns (0.00s)

解释一下上面的步骤:

// 1. deferred 匿名函数入栈
// x = i + 1 => x = 2
// y = j + 2 => y = 3
// 即 return 时 x = 2, y = 3
// 真正的退出函数之前,deferred 匿名函数开始执行 x = 2 * 5 , y = 4 * 10
// 最后 return x, y 返回值

5.defer + 调试日志

defer 可以实现在出入函数的时候打印调试级别的日志。

func trace(s string) string {
   fmt.Println("entering:", s)
   return s
}

func un(s string) {
   fmt.Println("leaving:", s)
}

func a() {
   defer un(trace("a"))
   fmt.Println("in a")
}

func b() {
   defer un(trace("b"))
   fmt.Println("in b")
   a()
}

func TestTraceLog(t *testing.T) {
   b()
}

// output
// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b

golang 中可以被 deferred 的函数

对于自定义的函数或方法,defer 可以给予无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。 Go 语言中内置的函数列表:

append cap close complex copy delete imag len
make new panic print println real recover

以上内置函数中可以被 deferred 的只有以下几种:

close copy delete print recover

但如果说你希望其余的内置函数同样可以被 deferred,你可以将该内置函数封装成一个匿名函数:

defer func() {
    _ = append(s1, 11)
}

注意 defer 的求值时机

defer 关键字后面的表达式是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。

func foo1() {
    for i := 0; i <= 3; i++ {
        defer fmt.Println(i)
    }
}

func foo2() {
    for i := 0; i <= 3; i++ {
        defer func(n int) {
               fmt.Println(n)
        }(i)
    }
}

func foo3() {
    for i := 0; i <= 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}

func main() {
    fmt.Println("foo1 result:")
    foo1()
    fmt.Println("\nfoo2 result:")
    foo2()
    fmt.Println("\nfoo3 result:")
    foo3()
}

先看输出日志,然后逐个分析 foo1、foo2、foo3 这三个函数的输出:

// output
foo1 result:
3
2
1
0

foo2 result:
3
2
1
0

foo3 result:
4
4
4
4

foo1 函数 for 循环在执行的时候 defer 注册函数入栈,依次注册了

fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
// LIFO 所以会打印 3 2 1 0

foo2 函数 for 循环在执行的时候注册了匿名的 deferred 函数,依次注册了

func(n int) {
    fmt.Println(n)
}(0)
func(n int) {
    fmt.Println(n)
}(1)
func(n int) {
    fmt.Println(n)
}(2)
func(n int) {
    fmt.Println(n)
}(3)
// LIFO 所以会打印 3 2 1 0

foo3 函数 for 循环在执行的时候同样注册了 3 个匿名的 deferred 函数,不过是没有带参数的,所以依次注册了

defer func() {
    fmt.Println(i)
}()
// 匿名函数以闭包的方式访问外围函数的变量i,并通过Println输出i的值,此时i的值为4
// LIFO 所以会一次打印 4 4 4 4

再来看一个例子

func foo4() {
   sl := []int{1, 2, 3}
   defer func(a []int) {
      fmt.Println(a)
   }(sl)

   sl = []int{3, 2, 1}
   _ = sl
}

func foo5() {
   sl := []int{1, 2, 3}
   defer func(p *[]int) {
      fmt.Println(*p)
   }(&sl)

   sl = []int{3, 2, 1}
   _ = sl
}

func TestFunc2(t *testing.T) {
   foo4()
   foo5()
}
// output
// [1 2 3]
// [3 2 1]

foo4 在执行 defer 的时候注册了匿名函数

defer func(a []int) {
    fmt.Println(a)
}([] int{1, 2, 3})
// 即匿名函数在注册时  a = []int{1, 2, 3}

此时 deferred 函数的入参已经确定为 []int{1, 2, 3},所以最后就算是 sl 的值重新赋值了,但并不影响到 deferred 函数的入参。 foo5 在执行 defer 的时候注册的匿名函数,其参数是一个地址,所以在后面 sl 被重新赋值时,由于地址是不变的,所以也就输出了[3, 2, 1]

参考资料