Go 语言
函数和方法
defer 函数

defer 函数

defer 函数是一个用于注册延迟调用的函数。这些调用直到调用者函数执行结束时才被执行。这个可以用于在函数执行结束后,执行一些清理工作。

defer 的运作离不开函数:

  • 在 Go 中,只有在函数和方法内部才能使用 defer 语句。
  • defer 关键字后面只能接函数或方法,这些函数被称为 deferred 函数。这些 deferred 函数按照退出前被按后进先出的(LIFO)的顺序调用。

我们来看一个例子:

func writeToFile(filename string, data string, mu *sync.Mutex) error {
    mu.Lock()
    defer mu.Unlock() // 在函数结束时解锁
 
    f, err := os.OpenFile(filename, 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() // 将文件内容刷到磁盘
}

常见用法

除了上面的例子展示的释放资源的用法外,defer 还有一些常见的用法。

拦截 panic

deferred 函数在出现 panic 的情况下,也会被执行。这个特性可以用于拦截 panic。我们来看下面的例子:

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("panic occurred:", err)
        }
    }()
 
    panic("something went wrong")
}
// 输出
// panic occurred: something went wrong

deferred 函数在 panic 之后被执行,所以我们可以在 deferred 函数中使用 recover 函数来拦截 panic。

也会有一些例外,例如通过 C 代码引起的崩溃,deferred 函数不会被执行。

修改具名返回值

我们来看一个例子:

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}
// 输出
// 1

输出调试信息

我们来看一下 Go 参考文档中一个实现:

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()
}
 
// 输出
// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b

这里要注意为什么 trace 会提前执行。这是因为在调用 defer 时,函数的参数就会被求值。所以 trace 函数会被提前执行。

还原变量旧值

这个用法来自 Go 标准库源码,在 sycall 包中有这样的代码:

func init() {
    oldFsinit := fsinit
    defer func() {
        fsinit = oldFsinit
    }()
 
    fsinit = func() {
        // ...
    }
    // ...
}

关键问题

defferd 函数的选择

对于自定义的函数和方法,defer 可以给予无条件支持,但对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候就会自动丢弃。

Go 语言还有内置函数,下面是 Go 语言内置函数列表:

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

上面这些内置函数,不能直接作为 deferred 函数的有:append、cap、complex、imag、len、make、new、real,剩下的如:close、copy、delete、panic、print、println、recover,可以作为 deferred 函数。

对于那些不能直接作为 deferred 函数的内置函数,可以通过定义一个匿名函数来调用这些内置函数,然后将这个匿名函数作为 deferred 函数。

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

defer 后表达式的求值时机

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

我们来看一个例子:

func foo1() {
    fmt.Println("foo1:")
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)
    }
}
 
func foo2() {
    fmt.Println("foo2:")
    for i := 0; i < 3; i++ {
        defer func(n int) {
            fmt.Println(n)
        }(i)
    }
}
 
func foo3() {
    fmt.Println("foo3:")
    for i := 0; i < 3; i++ {
        defer func() {
            fmt.Println(i)
        }()
    }
}
 
func main() {
    foo1()
    foo2()
    foo3()
}
 
// 输出
// foo1:
// 2
// 1
// 0
// foo2:
// 2
// 1
// 0
// foo3:
// 3
// 3
// 3

foo1 中,defer 将 fmt.Println(i) 压入栈时,都会对 i 进行求值。所以依次压入栈的函数是:

fmt.Println(0)
fmt.Println(1)
fmt.Println(2)

然后,依次弹出栈的函数是:

fmt.Println(2)
fmt.Println(1)
fmt.Println(0)

所以结果为 2 1 0

foo2 中,defer 将 func(n int) { fmt.Println(n) }(i) 压入栈时,都会对 i 进行求值。所以依次压入栈的函数是:

func(0)
func(1)
func(2)

foo1 类似,结果为 2 1 0

foo3 中,defer 后面接的是一个不带参数的匿名函数。所以依次压入栈的函数是:

func()
func()
func()

匿名函数以闭包的方式访问外围函数的变量 i,所以在执行时,i 的值已经变成了 3。所以结果为 3 3 3

defer 性能损耗

在 Go 1.12 之前,defer 语句的性能损耗是比较大的。使用 defer 的函数的执行时间是没有使用 defer 的函数的 7 倍。

在 Go 1.13 和 Go 1.14 版本之后,defer 性能提升巨大,已经和不用 defer 的性能相差很小。