defer 关键字的作用和用法,以及它与 panic 和 recover 的关系
defer 关键字可以用来在函数返回前执行一些清理或释放资源的操作,类似于 Java 和 C# 的 finally 语句块。defer 关键字后面跟着一个函数调用,这个函数调用会被延迟到当前函数返回时执行。如果当前函数发生了 panic 异常,defer 语句也会被执行,并且可以在 defer 函数中使用 recover 函数来捕获和处理 panic 异常。
例如1:
func main() {
// 声明一个匿名函数并赋值给变量 f
f := func() {
fmt.Println("defer begin")
// recover 函数的正确使用方式
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("defer end")
}
// 在 main 结束时调用上面声明的匿名函数
defer f()
fmt.Println("main begin")
panic("异常信息") // 引发 panic
fmt.Println("main end")
}
/*
输出:
main begin
defer begin
异常信息
defer end
exit status 2
*/
defer 关键字的三种实现方式:堆分配、栈分配和开放编码,以及它们各自的优缺点和性能影响
堆分配是早期 Go 语言处理 defer 关键字的方法,它会在堆上分配一个 runtime._defer 结构体来存储延迟函数和参数等信息,并将其链接到 Goroutine 的 _defer 链表中。这种方法的缺点是需要额外的内存分配和垃圾回收,以及链表遍历和解引用等操作,导致性能较差。
栈分配是 Go 语言在 1.13 版本引入的一种优化方法,它会在栈上分配一个 runtime._defer 结构体,并将其链接到 Goroutine 的 _defer 链表中。这种方法避免了堆上的内存分配和垃圾回收,但仍然需要链表遍历和解引用等操作。
开放编码是 Go 语言在 1.14 版本引入的另一种优化方法,它会将 defer 关键字后面的函数调用直接内联到当前函数中,并在适当的位置插入必要的代码来保证延迟执行。这种方法消除了 runtime._defer 结构体和 _defer 链表的使用,使得 defer 关键字几乎没有额外开销。
defer 关键字在编译时的转换规则,以及它如何影响函数调用栈帧的结构
defer 关键字在编译时会被转换为对 runtime.deferproc 和 runtime.deferreturn 的调用。runtime.deferproc 负责注册延迟函数,并返回一个布尔值表示是否需要继续执行当前函数;runtime.deferreturn 负责执行延迟函数,并返回一个布尔值表示是否需要恢复 panic 异常。
defer 关键字会影响函数调用栈帧(stack frame)的结构,在栈帧中增加了两个字段:_panic 和 _defer。_panic 字段存储了当前 Goroutine 发生 panic 异常时产生的 runtime._panic 结构体;_defer 字段存储了当前 Goroutine 最新注册的 runtime._defer 结构体。
defer 关键字在运行时的执行过程
defer 关键字在运行时的执行过程分为两个阶段:注册阶段和调用阶段。注册阶段是指 defer 关键字所在的语句被执行时,会调用 runtime.deferproc 函数来创建一个 runtime._defer 结构体,并将其链接到 Goroutine 的 _defer 链表中。调用阶段是指当前函数返回前,会调用 runtime.deferreturn 函数来遍历 _defer 链表,并依次执行每个 runtime._defer 结构体中存储的延迟函数。
defer 关键字会按照先进后出(FILO)的顺序执行延迟函数,也就是说最后注册的延迟函数会最先被执行,最先注册的延迟函数会最后被执行。这样可以保证资源的正确释放顺序,例如关闭文件、解锁互斥量等。
defer 关键字会在注册阶段就对延迟函数和参数进行求值,并将结果存储到 runtime._defer 结构体中。这样可以保证延迟函数和参数在调用阶段不受外部变化的影响,例如变量被重新赋值等。
defer 关键字可以修改当前函数的返回值,在调用阶段通过修改栈帧中存储的返回值来实现。这样可以实现一些特殊的功能,例如捕获错误、记录日志等。
package main
import (
"fmt"
)
func add(a, b int) (result int) {
defer func() {
result += 10 // 在返回之前增加10
}()
result = a + b // 计算a和b的和
return // 返回result
}
func main() {
fmt.Println(add(1, 2)) // 打印13
}
defer 关键字可以与 panic 和 recover 配合使用,在调用阶段通过检查 _panic 字段来判断是否发生了 panic 异常,并通过调用 recover 函数来恢复正常状态。这样可以实现一些异常处理和错误恢复的逻辑,例如释放资源、打印堆栈信息等。
package main
import "fmt"
func main() {
fmt.Println("main begin")
fmt.Println(f1())
fmt.Println("main end")
}
func f1() (result int) {
// 延迟匿名函数
defer func() {
// 修改返回值
result++
// 捕获 panic 异常
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 延迟普通函数
defer f2()
// 引发 panic 异常
panic("panic error")
return result
}
func f2() {
fmt.Println("f2 begin")
// 修改外部变量(无效)
result := -1
fmt.Println("f2 end")
}
/*
输出:
main begin
f2 end
f2 begin
panic error
f2 end
-1
main end
*/