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 的性能相差很小。