defer的作用
defer声明的函数在当前函数return之后执行,通常用来做资源、连接的关闭和缓存的清除等。
A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.
注意:函数的return其实分为两部分:1将要返回的值赋值给返回值地址空间,2返回返回地址空间中的值。而defer发生在这两个操作中间,会造成一些defer不生效的错觉。详见go函数调用
defer原理
使用defer关键字声明的方法,实际上编译器会转换为两个方法的调用:
- runtime.deferproc:将defer函数注册到goroutine中
- runtime.deferreturn:在外层函数的末尾添加,执行注册的defer
我们都知道注册了多个defer函数后,defer的执行顺序是倒序的,类似栈的后进先出特性,实际上go使用链表实现的defer,每次新注册的defer都是头插法注册到链表中:
// 移除了无关代码
func deferproc(fn func()) {
//获取当前g
gp := getg()
//新建defer结构
d := newdefer()
//将新创建的d添加到defer链表头
d.link = gp._defer
gp._defer = d
d.fn = fn
}
// defer结构体中有指向下一个defer的指针
type _defer struct {
started bool //defer是否开始执行
heap bool //是否堆分配
openDefer bool //是否是open coded
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // 触发当前defer的panic
link *_defer // defer链表
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}
// g结构体持有defer链表的头指针
type g struct{
_defer *_defer
}
嵌套defer
嵌套的defer依然按照顺序来看:先注册A中的A1,然后defer注册B,B中的defer顺序为B1->B2,最后注册A2。现在defer链表中是这样的:A2->B2->B1->A1。
func A() {
defer A1()
defer B()
defer A2()
}
func B() {
defer B1()
defer B2()
}
func A1() {
fmt.Println("defer A1")
}
func A2() {
fmt.Println("defer A2")
}
func B1() {
fmt.Println("defer B1")
}
func B2() {
fmt.Println("defer B2")
}
func main() {
A()
}
//输出
//defer A2
//defer B2
//defer B1
//defer A1
defer与panic
GO语言使用error机制来表示程序异常,而panic一般用于表示严重的错误出现。当发生panic时,如果没有使用recover来捕捉panic,那么程序就会退出。我们在日常开发中一般不会使用panic,因为大多数时候我们都希望程序能够运行下去,而不是直接停止服务。
当发生panic时,panic之后的代码不会执行,但是会触发之前注册的defer:
func A() {
defer A1()
panic("panic A")
defer A3()
}
func A1() {
fmt.Println("defer A1")
}
func A3() {
fmt.Println("defer A3")
}
func main() {
A()
}
/** 输出结果
defer A1
panic: panic A
**/
可以看到发生panicA时,触发了A中已经注册的defer A1,但是A3还未注册,所以不会执行。在同一个方法里只会有一个panic生效,因为后续代码不会执行了。
panic结构体
type _panic struct {
argp unsafe.Pointer // 当前panic要执行的defer函数参数
arg any // panic的参数
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // panic是否被恢复
aborted bool // panic是否终止
goexit bool
}
// g中也保存panic指针
type g struct{
_panic *_panic
}
当发生一个panic时,当前的panic会被加入g的panic链表。然后执行defer链表。
多个panic和defer嵌套
func A() {
defer A1()
defer A2()
panic("panic A")
}
func A1() {
fmt.Println("defer A1")
panic("panic A1")
}
func A2() {
fmt.Println("defer A2")
}
func main() {
A()
}
/**
defer A2
defer A1
panic: panic A
panic: panic A1
**/
这段代码中的执行逻辑:
- 注册defer A1和A2,现在的defer链表是:A2->A1
- 触发panic A,将panic A加入panic链表头
- 执行defer链表,正常执行defer A2,然后将A2移出defer链表
- 执行defer A1,此时A1中又触发了panic,不能正常执行完成,将A1的
_defer.started
标记为true,并将_defer._panic
记为panic A。此时defer链表中只有一个started为true的defer A1 - 将新触发的panic A1加入到panic链表头,然后去执行defer链表,发现A1的started为true,且_panic是panic A。将panic A的aborted设为true,标记为终止。同时移除defer A1。
- 打印panic信息,从链表尾开始,panic A->panic A1
有recover的情况
recover的工作只是将panic的recovered字段设置为true。
先看一个简单的例子:
func A() {
defer A1()
defer A2()
panic("panic A")
}
func A1() {
fmt.Println("defer A1")
}
func A2() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("defer A2")
}
func main() {
A()
}
/**
输出结果
panic A
defer A2
defer A1
**/
这段代码的执行逻辑:
- 注册defer A1和A2
- 运行到panic时,将panic A加入panic链表。然后执行defer链表,先执行A2
- 执行A2时触发了recover,将panic A的recovered字段设为true,执行recover分支中的打印"panic A",然后继续执行A2中的打印语句"defer A2"
- 在defer A2执行完成后,go会检查当前panic已经recovered了,于是将panic A移出panic链表,然后通过_defer.sp和_defer.pc两个指针来找到要跳转的指令,最后将defer A2移出链表。
- 根据sp和pc跳回到defer执行逻辑,继续执行defer A1,打印出"defer A1"
多个panic
func A() {
defer A2()
defer A1()
panic("panic A")
}
func A1() {
fmt.Println("defer A1")
panic("panic A1")
}
func A2() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("defer A2")
}
func main() {
A()
}
/**
输出:
defer A1
panic A1
defer A2
**/
上面的代码在defer A1中也抛出了panic,多个panic同时存在时,recover只会捕捉到最新的。也只需要捕获一次。
recover之后又发生了panic
func A() {
defer A2()
defer A1()
panic("panic A")
}
func A1() {
fmt.Println("defer A1")
}
func A2() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("defer A2")
panic("panic A1")
}
func main() {
A()
}
/**
输出:
defer A1
panic A
defer A2
panic: panic A [recovered]
panic: panic A1
**/
先触发的panic A被recover捕获到,打印的时候会标识为[recovered],再次触发的panic A1由于后续没有recover处理,所以程序终止。
defer的逐步优化
1.12版本中的defer是在堆中分配的,虽然golang实现了defer池可以复用defer结构体,但是效率依然不理想。在1.13版本中加入了defer栈上分配的功能,这样defer的性能就快很多。
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
// fn is already set.
// The other fields are junk on entry to deferprocStack and
// are initialized here.
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// The lines below implement:
// d.panic = nil
// d.fd = nil
// d.link = gp._defer
// gp._defer = d
// But without write barriers. The first three are writes to
// the stack so they don't need a write barrier, and furthermore
// are to uninitialized memory, so they must not use a write barrier.
// The fourth write does not require a write barrier because we
// explicitly mark all the defer structures, so we don't need to
// keep track of pointers to them with a write barrier.
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
1.14中加入了open coded defer,这使得我们在使用defer时不一定要创建defer结构体并加入到defer链表中,而是直接在编译阶段在函数体内加上相应的逻辑。
参考B站幼麟实验室