关于golang:Go进阶基础特性defer

36次阅读

共计 3900 个字符,预计需要花费 10 分钟才能阅读完成。

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。

正文完
 0