2

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 结构体,这里包含三种路径:

  1. 从调度器的延迟调用缓存池 sched.deferpool 中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中;
  2. 从 Goroutine 的延迟调用缓存池 pp.deferpool 中取出结构体;
  3. 通过 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。

与昊
225 声望636 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道