1. 前言
defer 是 go 语言的关键字,被 defer 修饰的 func,将会在函数返回之前执行。
defer 具有以下特点
- 延迟执行
- 参数预计算
- 同一 goroutine 中多个 defer 的执行顺序依照 FILO 规则
2. 使用场景
2.1. 资源释放
下边的程序是打开文件,并在执行一系列操作后,将文件关闭。
func readFile() {
src,err := os.Open("filename")
if err != nil {
return
}
// do something
src.Close()
}
代码通过 os.Open 打开一个文件,在执行一系列操作后,在最后通过 src.Close 方法将资源释放。正常情况下,该程序都能够正常运行到 src.Close。但在执行一系列操作过程中,出现异常并退出程序后,文件资源将无法被释放。一般,我们期望在程序退出前能够执行资源释放操作,而使用 defer 能达到这种目的。将上边程序修改如下,在成功打开文件之后,无论后边程序是否能够正常运行 ,在退出前,都能够将文件资源释放。
func readFile() {
src,err := os.Open("filename")
if err != nil {
return
}
defer src.Close()
// do something
}
同样的问题还会发生在使用 lock, channel 的情况下,一旦加锁后发生错误,将可能导致严重的死锁问题,多次创建未被 close 掉的 channel,长期下来,将导致严重的内存泄漏问题。
2.2. 异常捕获
程序可能存在 panic ,如除 0 运算、数组越界访问 、对空指针取值等,这些异常将导致程序退出。某些时候,我们期望捕获这些异常,并让程序正常执行。下边为模拟程序运行过程中发生 panic。
func doPanic() {
panic("panic")
fmt.Println("do panic success")
}
将会得到输出如下,terminal 将会打印出程序异常退出栈追踪信息 。
panic: panic
goroutine 1 [running]:
main.doPanic(...)
/Users/xxx/Documents/workspace/go/notecode/main.go:29
main.main()
/Users/xxx/Documents/workspace/go/notecode/main.go:8 +0x27
exit status 2
若期望程序能够正常运行,可将程序修改如下:
func doPanic() {
defer func(){
if err := recover();err != nil {
fmt.Println("recover")
}
}()
panic("panic")
fmt.Println("do panic success")
}
输出如下,表明程序异常后,仍能被正常调用执行。
recover
3. 原理解析
本文使用的 go version 为 go1.17.10
3.1. defer 结构定义
每个协程对象中,存在一个 _defer 指针,该指针指向与该协程相关联的defer,每个 defer 的调用只与当前协程有关 ,与其它协程无关。
type g struct {
_defer *_defer
}
_defer 结构定义在 runtime/runtime2.go 文件中,具体定义如下:
type _defer struct {
siz int32 // 参数大小
started bool // 当前 defer 是否开始被执行
heap bool // 是否被分配到堆中
openDefer bool // 是否开放式编程
sp uintptr // sp 寄存器
pc uintptr // pc 寄存器
fn *funcval // defer 运行的 func
_panic *_panic
link *_defer // 下一个 _defer
// 与开放式编程相关的参数
fd unsafe.Pointer
varp uintptr
framepc uintptr
}
多个 defer 与 goroutine 的关联如下:
3.2. 延迟执行
defer 后的函数不会立即执行,而是待函数执行结束后再调用。
func doDefer() {
defer func() {
fmt.Println("do defer")
}()
fmt.Println("doDefer normal")
}
输出结果如下,说明 defer 后的函数待函数执行返回时才执行,因此可以很好地用作资源释放。
doDefer normal
do defer
通过 go tool compile -N -l main.go
+ go tool objdump main.o
运行函数,观察main.go 的汇编情况。
在执行 doDefer 函数的过程中,先是调用 runtime.deferprocStack() 方法,待 fmt.Println()打印结束后,再执行 runtime.deferreturn。编译器会自动在函数的最后插入 deferreturn, 这也是 defer 为何具有延迟执行特性的原因。
打开 runtime/panic.go 文件, 查看 deferprocStack 函数的内容,如下:
func deferprocStack(d *_defer) {
gp := getg() // 获取 g,判断是否在当前栈上
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
if goexperiment.RegabiDefer && d.siz != 0 {
throw("defer with non-empty frame")
}
// siz and fn are already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
// defer 未开始
d.started = false
// defer 不在堆上
d.heap = false
// defer 没有开放式编程
d.openDefer = false
// 设置 sp
d.sp = getcallersp()
// 设置 pc
d.pc = getcallerpc()
// 没有开放式编程,framepc,varp 等属性都是为 0
d.framepc = 0
d.varp &#