Golang 中 defer 关键字可以将函数或方法注册到 goroutine 用于存放 deferred 函数的栈数据结构中。之后在函数退出之前(return 或者 panic)按照先进后出(LIFO)的顺序调度执行。

defer 常用场景

Golang defer usecases

1. 拦截 panic

// $GOROOT/src/bytes/buffer.go
func makeSlice(n int) []byte {
  // If the make fails, give a known error.
  defer func() {
      if recover() != nil {
          panic(ErrTooLarge) // 触发一个新panic
      }
  }()
  return make([]byte, n)
}

defer function 中是用 recover 是 Golang 中唯一从 panic 中恢复的手段,能够拦截运行时中的 panic,但是对运行时之外触发的崩溃无法捕获,比如调用 C 语言的库,C 语言中发生的崩溃。

2. 修改函数的具名返回值

下面是一个标准库中的例子:

// $GOROOT/src/fmt/scan.go
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
    defer func() {
        if e := recover(); e != nil {
            if se, ok := e.(scanError); ok {
                err = se.err
            } else {
                panic(e)
            }
        }
    }()
    ...
}

3. 输出调试信息

下面这个例子通过 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 main() {
    b()
}

4. 还原变量旧值

下面同样是一个来自标准库的例子:

// $GOROOT/src/syscall/fs_nacl.go
func init() {
    oldFsinit := fsinit
    defer func() { fsinit = oldFsinit }()
    fsinit = func() {}
    Mkdir("/dev", 0555)
    Mkdir("/tmp", 0777)
    mkdev("/dev/null", 0666, openNull)
    mkdev("/dev/random", 0444, openRandom)
    mkdev("/dev/urandom", 0444, openRandom)
    mkdev("/dev/zero", 0666, openZero)
    chdirEnv()
}
指向原始笔记的链接

defer 支持哪些函数

所有自定义的函数和方法都能支持,对于有返回值的函数在 deferred 调用时返回值会被丢弃。因此对于有些需要接住返回值的内置函数,Golang 编译时候检查到会报错,毕竟这样的调用没有意义。

defer 后表达式的求值时机

defer 的函数会在 deferred 栈执行的时候调用,但是函数的参数是在 defer 时求值的,比如 Golang defer usecases ❯ 3. 输出调试信息 这个例子。

defer 有性能损耗

在 go1.14 之前 defer 和不使用 defer 性能差可达到 7 倍左右,1.14 之后做了很大优化,差距很小了。