defer 是 Golang 中一个非常重要的关键字,其在函数返回之前进行调用,进行资源回收、错误处理等工作。

触发时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

阅读官方文档可知,defer 的触发时机主要有三个:

  • 函数执行到函数体末端
  • 函数执行到 return 语句
  • 当前协程 panic
// func
// defer
func test1() {
    defer fmt.Println("defer")
    fmt.Println("func")
}

// defer
func test2() {
    defer fmt.Println("defer")
    return
}

// defer
// panic
func test3() {
    defer fmt.Println("defer")
    panic("panic")
    fmt.Println("func")
    defer fmt.Println("defer")
}

留意 test3 函数的输出可知:当任意 goroutine 发生 panic 时,会执行当前协程中 panic 之前已声明的 defer 。

执行顺序

若函数中有多个 defer ,执行顺序为 先进后出 ,可理解为栈。

// 3
// 2
// 1
func test()  {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
}

参数确定

defer 在声明时会先计算确定其参数的值,推迟执行的仅是其函数体。同时,defer 满足闭包和匿名函数的特性。

// 5
func test1() (res int) {
    defer func(res int)  {
        res++
    }(res)
    return 5
}

// 1
func test2() {
    i := 1
    defer fmt.Println(i)
    i = 3
}

// 3
func test3() {
    i := 1
    defer func() {
        fmt.Println(i)
    }()
    i = 3
}

返回值处理

在调用 return 时,会完成以下的事情:

  • 对返回值赋值
  • 调用 defer 表达式
  • 退出函数,并将返回值给调用函数

匿名返回值

对于匿名返回值函数,其返回值在代码中通常是无法获取的。

// 1
func test() int {
    a := 1
    defer func() {
        a = 2
    }()
    return a
}

当返回值是指针类型时,我们还是可以修改返回值的。

// 2
func test() *int {
	a := 1
	p := &a
	defer func() {
		*p = 2
	}()
	return &a
}

此外,defer 函数的返回值是会直接被抛弃的

// 1
func test() int {
    res := 1
    defer func() int {
        res++
        return res
    }()
    return 1
}

命名返回值

对于命名返回值函数,可以在 defer 中修改返回值变量,进而修改函数的返回值。

// 2
func test1() (res int) {
    res = 1
    defer func() {
        res = 2
    }()
    return
}

// 2
func test2() (res int) {
    i := 1
    defer func() {
        res++
    }()
    return i
}

注意点

  • 主动调用 os.Exit(int) 时,defer 将不执行。
  • 在发生 panic 的 goroutine 中,若没有一个 defer 调用 recover() 进行修复,则在执行完之前已声明的 defer 后,进程崩溃。
  • defer 只对当前 goroutine 有效,因此当一个没有 defer 调用 recover() 的 goroutine 发生了 panic ,即使其他 goroutine 有 defer 调用 recover() ,进程依旧会崩溃。

参考资料