Golang defer

作用

延迟调用

被defer的函数调用会延迟到当前函数结束的时候再进行调用,defer调用完毕后,函数的返回值才会被返回给上一级调用者。

比如这样的例子

package main

func main() {
    defer println(1)
    println(2)
}

其输出的结果是

2
1

后进先出调用

对defer的多次调用,是后进先出的压栈式的调用,也就是说,最后的一个defer会最先调用。

比如

package main

func main() {
    for i:=0; i<3; i++ {
        defer println(i)
    }
}

输出结果如下:

2
1
0

defer用做recover

defer除了日常作为随后保证关闭资源的操作外,还会作为异常recover。

package main

func main() {
    defer func() {
        if e := recover(); e != nil {
            println("recovered")
        }
    }()

    panic("gone")
}

输出结果如下:

recovered

defer 如何起作用

我们把有defer的代码反汇编,那么可以看到如下的输出:

go tool compile -S ./main.go
...
        0x0035 00053 (./main.go:5)      MOVL    $8, (SP)
        0x003c 00060 (./main.go:5)      PCDATA  $0, $1
        0x003c 00060 (./main.go:5)      LEAQ    "".wrap·1·f(SB), CX
        0x0043 00067 (./main.go:5)      PCDATA  $0, $0
        0x0043 00067 (./main.go:5)      MOVQ    CX, 8(SP)
        0x0048 00072 (./main.go:5)      MOVQ    AX, 16(SP)
        0x004d 00077 (./main.go:5)      CALL    runtime.deferproc(SB)
        0x0052 00082 (./main.go:5)      TESTL   AX, AX
        0x0054 00084 (./main.go:5)      JNE     88
        0x0056 00086 (./main.go:5)      JMP     33
        0x0058 00088 (./main.go:5)      XCHGL   AX, AX
        0x0059 00089 (./main.go:5)      CALL    runtime.deferreturn(SB)
        0x005e 00094 (./main.go:5)      MOVQ    32(SP), BP
        0x0063 00099 (./main.go:5)      ADDQ    $40, SP
        0x0067 00103 (./main.go:5)      RET
...

我们可以看到,有defer的地方被拆成了两个步骤来运行,runtime.deferprocruntime.deferreturn

defer println(1)这句话被编译成了两个过程,先执行runtime.deferproc生成println函数及其相关参数的描述结构体, 然后将其挂载到当前g的_defer指针上。

deferproc

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
    if getg().m.curg != getg() {
        // go code on the system stack can't defer
        throw("defer on system stack")
    }

    // the arguments of fn are in a perilous state. The stack map
    // for deferproc does not describe them. So we can't let garbage
    // collection or stack copying trigger until we've copied them out
    // to somewhere safe. The memmove below does that.
    // Until the copy completes, we can only call nosplit routines.
    sp := getcallersp(unsafe.Pointer(&siz))
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc() // 存储的是 caller 中,call deferproc 的下一条指令的地址

    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:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }

    // deferproc returns 0 normally.
    // a deferred func that stops a panic
    // makes the deferproc return 1.
    // the code the compiler generates always
    // checks the return value and jumps to the
    // end of the function if deferproc returns != 0.
    return0()
    // No code can go here - the C return register has
    // been set and must not be clobbered.
}

关键的地方是那个newdefer,名字很好,一眼就让我们抓到了重点。

func newdefer(siz int32) *_defer {
    var d *_defer
    sc := deferclass(uintptr(siz))
    gp := getg()
    if sc < uintptr(len(p{}.deferpool)) {
        // 从 p 结构体的 deferpool 中获取可用的 defer struct
        // 代码比较简单,省略
    }
    if d == nil {
        // 上面没有成功获取到可用的 defer struct
        // 因此需要切换到 g0 生成新的 defer struct
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
    }
    // defer func 的参数大小
    d.siz = siz
    // 链表链接
    // 后 defer 的在前,类似一个栈结构
    d.link = gp._defer
    // 修改当前 g 的 defer 结构体,指向新的 defer struct
    gp._defer = d
    return d
}

newdefer将一个重要的_defer结构体挂到了goroutine的defer链上。 中间大段的注释显示了这与recover的流程关系非常大。

type _defer struct {
    siz     int32    // 函数的参数总大小
    started bool     // TODO defer 是否已开始执行?
    sp      uintptr  // 存储调用 defer 函数的函数的 sp 寄存器值
    pc      uintptr  // 存储 call deferproc 的下一条汇编指令的指令地址
    fn      *funcval // 描述函数的变长结构体,包括函数地址及参数
    _panic  *_panic  // 正在执行 defer 的 panic 结构体
    link    *_defer  // 链表指针
}

deferreturn

deferreturn会先判断链表中是否有defer,然后jmpdefer去做defer该干的事情, 然后,jmpdefer会的跳回deferreturn之前,如果此时defer链表中还有未处理的defer,那么就再来一把, 如果链表空了,那就return,defer的处理也就结束了。

// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
// Cannot split the stack because we reuse the caller's frame to
// call the deferred function.

// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) {
    gp := getg()
    d := gp._defer
    if d == nil {
        return
    }
    sp := getcallersp(unsafe.Pointer(&arg0))
    if d.sp != sp {
        return
    }

    // Moving arguments around.
    //
    // Everything called after this point must be recursively
    // nosplit because the garbage collector won't know the form
    // of the arguments until the jmpdefer can flip the PC over to
    // fn.
    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link
    freedefer(d)
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

我们来看看对链表进行循环遍历的jmpdefer

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
    MOVQ    fv+0(FP), DX      // defer 的函数的地址
    MOVQ    argp+8(FP), BX    // caller sp
    LEAQ    -8(BX), SP        // caller sp after CALL
    MOVQ    -8(SP), BP        // 在 framepointer enable 的时候,不影响函数栈的结构
    SUBQ    $5, (SP)          // call 指令长度为 5,因此通过将 ret addr 减 5,能够使 deferreturn 自动被反复调用
    MOVQ    0(DX), BX
    JMP     BX                // 调用被 defer 的函数

哈,我们可以看到,在jmpdefer所调用的函数返回时,会回到调用deferreturn的函数, 并重新执行deferreturn,每次执行都会使g的defer链表表头被消耗掉, 直到进入deferreturn时d == nil并返回。至此便完成了整个defer的流程。

这里比较粗暴的是直接把栈上存储的pc寄存器的值减了5, 注释中说是因为call deferreturn这条指令长度为5,这是怎么算出来的呢:

        0x0059 00089 (./main.go:5)      CALL    runtime.deferreturn(SB)
        0x005e 00094 (./main.go:5)      MOVQ    32(SP), BP

这条指令长度就是5,所以这里是用汇编非常trick的实现了一个for循环…

那么deferreturn + jmpdefer就可以使_defer链表被消费完,那为什么还要编译出多次的deferreturn调用呢? 可能是因为deferproce和deferreturn是成对出现的,这样做可能比较容易实现吧。

总结

defer是一个面向编译器的声明,他会让编译器做两件事:

  1. 编译器会将defer声明编译为runtime.deferproc(fn),这样运行时,会调用runtime.deferproc,在deferproc中将所有defer挂到goroutine的defer链上
  2. 编译器会在函数return之前,增加runtime.deferreturn调用;这样运行时,开始处理前面挂在defer链上的所有defer。