5

Golang defer

 2 years ago
source link: https://ray-g.github.io/golang/golang_defer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Golang defer

Nov 6, 2017 5 min read

被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。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK