defer 是我们经常会使用的一个关键字,它会在当前函数返回前执行传入的函数,常用于关闭文件描述符、关闭数据库连接以及解锁资源。
使用场景
释放资源
这是 defer 最常见的用法,包括释放互斥锁、关闭文件句柄、关闭网络连接、关闭管道和停止定时器等,如:
m.mutex.Lock()
defer m.mutex.Unlock()
异常处理
defer 第二个重要用途就是处理异常,与 recover 搭配一起处理 panic,让程序从异常中恢复。例如 gin 框架中 recovery 中间件的源码:
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// ...
修改命名返回值
// $GOROOT/src/fmt/scan.go
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
defer func() {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {
err = se.err
} else {
panic(e)
}
}
}()
...
}
打印调试信息
// $GOROOT/src/net/conf.go
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
}()
}
...
}
行为规则
defer 的语法很简单,不过衍生出的用法很多,有时让人迷惑,在这里我们总结一下 defer 的几个基本使用规则。
同一函数内部不同 defer 关键字后面的函数是逆序执行的
后面我们会讨论,defer 关键字后面的延迟函数需要注册到一个 deferred 函数栈中(本质上是一个链表),因此遵循栈的后进先出规则,多个 defer 后面的函数是逆序执行的。
defer 关键字后面的函数参数是在 defer 关键字出现时预计算的
defer 关键字后面的函数是在注册到 deferred 函数栈的时候进行求值的。下面看一个例子:
func test1() {
for i := 0; i <= 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i)
}
}
func test2() {
for i := 0; i <= 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
func main() {
test1()
test2()
}
在 test1 中,defer 后面接的是一个带有一个参数的匿名函数。每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值。因此,deferred 函数栈中匿名函数的参数依次是 0,1,2,3,打印出来的结果就是 3,2,1,0。
在 test2 中,defer 后面接的是一个不带参数的匿名函数。当代码执行的时候,deferred 栈中弹出的函数会以闭包的方式访问外部变量 i,而此时 i 的值已经变为了 4,因此打印结果为 4,4,4,4。
实现原理
数据结构
// src/runtime/runtime2.go
type _defer struct {
siz int32
started bool
heap bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
- siz 是参数和结果的内存大小;
- heap 表示该结构体是否存于堆中;
- sp 和 pc 分别代表栈指针和调用方的程序计数器;
- fn 是 defer 关键字中传入的函数;
- _panic 是触发延迟调用的结构体,可能为空;
- openDefer 表示当前 defer 是否经过开放编码的优化。
可以看出,每个 _defer 实例实际上是对一个函数的封装,拥有执行函数的必要信息,如栈指针等。多个 _defer 实例使用指针 link 连接起来形成一个单向链表,保存到当前 goroutine 的数据结构中,待当前函数执行结束再逐个取出执行。
type g struct {
// ...
_defer *_defer
// ...
}
执行机制
在中间代码生成阶段会处理程序中的 defer,根据不同的条件,会有三种不同的机制来处理该关键字:堆分配、栈分配和开放编码。早期的 Go 语言会在堆上分配 _defer 结构体,不过该实现的性能较差,在 1.13 版本中引入栈上分配的结构体,减少了 30% 的额外开销,然后在 1.14 中引入了基于开放编码的 defer,使得性能大幅度提升。
堆分配
堆分配是默认的兜底方案,采用本方案时,在编译期间会将 defer 关键字转换成 runtime.deferproc 函数,并且为所有调用 defer 的函数末尾插入 runtime.deferreturn 函数。简而言之,就是 deferproc 生成 _defer 结构体并插入链表,deferreturn 取出 _defer 并执行。
来简单看一下源码:
func deferproc(siz int32, fn *funcval) {
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
deferproc 会为 defer 创建一个新的 _defer 结构体、设置它的函数指针 fn、程序计数器 pc 和栈指针 sp 并将相关的参数拷贝到相邻的内存空间中。
newdefer 的作用是获取 _defer 结构体,这里包含三种路径:
- 从调度器的延迟调用缓存池 sched.deferpool 中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中;
- 从 Goroutine 的延迟调用缓存池 pp.deferpool 中取出结构体;
- 通过 runtime.mallocgc 在堆上创建一个新的结构体。
无论使用哪种方式,获取到 _defer 结构体之后,都会被追加到所在 Goroutine 的 _defer 链表的最前面。
栈分配
栈分配 defer 是为了提高堆分配 defer 的内存使用效率而引入的,当 defer 关键字在函数中最多执行一次时,编译器就会将 defer 编译成 deferprocStack 函数将 _defer 结构体分配到栈上。
在编译期间已经创建了 _defer 结构体,所以 deferprocStack 只需要设置一些未初始化的字段,然后将栈上的 _defer 追加到链表上。
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
*(*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()
}
开放编码
无论是堆分配 defer 还是栈分配 defer,编译器都只能把 defer 转换成相应的创建 _defer 结构体的函数,最后通过 deferreturn 函数取出结构体再执行。如果编译器不这么麻烦,直接把 defer 语句转换成相应的代码插入函数尾部,是不是就可以节省很多步骤,提高存储效率和性能?开放编码使用的就是这种思路。
但并不是所有的情况下都可以使用开放编码方式,在一下场景下 defer 语句不能被处理成开放编码类型:
- 编译时禁用了编译器优化;
- defer 出现在循环中;
- 单个函数中 defer 出现了 8 个以上;
- 单个函数中 return 语句的个数乘以 defer 语句的个数超过了 15。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。