【Free Style】深入Go语言模型(4):defer的底层实现与使用注意事项

举报
霜雯 发表于 2017/11/09 11:12:09 2017/11/09
【摘要】 前言defer是Go语言中一个关键字, 主要提供延迟调用的能力,defer主要用在资源释放,会在函数返回之前进行调用。一般的调用方式如下:func testFile(fileName string) { f, err := os.Open(fileName) if err != nil { //handle error } defer f.Close()

image.png

defer的底层实现

注:后面的基于Go 1.8进行分析

对于下面的代码:

func add(a, b int) int {

    defer func() {

        fmt.Println("defer-add")

    }()

 

    return a + b

}

我们查看生成的汇编代码如下:

TEXT    "".add(SB), $24-24

MOVQ    TLS, CX

MOVQ    (CX)(TLS*2), CX

CMPQ    SP, 16(CX)

JLS    123

SUBQ    $24, SP

MOVQ    BP, 16(SP)

LEAQ    16(SP), BP

FUNCDATA    $0, gclocals·54241e171da8af6ae173d69da0236748(SB)

FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)

MOVQ    $0, "".~r2+48(FP)

MOVL    $0, (SP)

LEAQ    "".add.func1·f(SB), AX

MOVQ    AX, 8(SP)

PCDATA    $0, $0

CALL    runtime.deferproc(SB)

TESTL    AX, AX

JNE    107

MOVQ    "".b+40(FP), AX

MOVQ    "".a+32(FP), CX

ADDQ    CX, AX

MOVQ    AX, "".~r2+48(FP)

PCDATA    $0, $0

XCHGL    AX, AX

CALL    runtime.deferreturn(SB)

MOVQ    16(SP), BP

ADDQ    $24, SP

RET

在上面的汇编代码中有两个特别的调用:
runtime.deferproc(SB)
runtime.deferreturn(SB),这个就是defer底层的实现的关键。

deferproc的实现

deferproc先通过newdefer函数来生成一个Defer对象,封装调用函数的一些信息,然后把它加到goroutineg对象的defer链表中。【关于goroutineg数据结构后面再单独讲,这里就不展开了】

// deferred subroutine calls

type _defer struct {

    siz     int32

    started bool

    sp      uintptr // sp at time of defer

    pc      uintptr

    fn      *funcval

    _panic  *_panic // panic that is running defer

    link    *_defer

}

func newdefer(siz int32) *_defer {

    var d *_defer

    sc := deferclass(uintptr(siz))

    gp := getg()

 

    if sc < uintptr(len(p{}.deferpool)) {

        //....

        pp := gp.m.p.ptr()

        d := sched.deferpool[sc]

        pp.deferpool[sc] = append(pp.deferpool[sc], d)

 

        //...

    }

 

    if d == nil {

        // Allocate new defer+args.

        systemstack(func() {

            total := roundupsize(totaldefersize(uintptr(siz)))

            d = (*_defer)(mallocgc(total, deferType, true))

        })

    }

    d.siz = siz

    d.link = gp._defer

    gp._defer = d  // g_defer永远指向最后一个新增defer对象

    return d

}

上面的getg函数就是返回当前g对象的地址.

defer的释放是通过freedefer函数来尽心的

我们再来看看deferproc是怎么实现的:

func deferproc(siz int32, fn *funcval) {

    //...

    d := newdefer(siz)

    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))

    }

 

    return0()

}

从上面可以runtime·deferproc并没有调用函数。实际上,defer函数是在runtime·deferreturn中完成调用的,runtime·deferreturn会调用G.defer链表中的所有Defer对象封装的函数。

deferreturn的实现

func deferreturn(arg0 uintptr) {

    gp := getg()

    d := gp._defer

    if d == nil {

        return

    }

 

    sp := getcallersp(unsafe.Pointer(&arg0))

    // d.sp为调用者在调用deferproc之前的栈指针SP,通过比较这两个地址,就可以确定是否是同

    // 一个调用函数的栈。如果不同,说明Defer不属于当前调用函数,从而中断deferreturn的循环调用

    //这里就保证了不是当前函数defer不会被调用

    if d.sp != sp {

        return

    }

 

 

    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)//释放defer对象

 

    //执行defer中的函数,并且递归调用当前g下面所有的defer

    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))

我们再来看一下jumdefer的实现:

// void jmpdefer(fn, sp);

// called from deferreturn.

// 1. pop the caller

// 2. sub 5 bytes from the callers return

// 3. jmp to the argument

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16

    MOVQ    fv+0(FP), DX    // fn

    MOVQ    argp+8(FP), BX    // caller sp

    LEAQ    -8(BX), SP    // caller sp after CALL

    MOVQ    -8(SP), BP    // restore BP as if deferreturn returned (harmless if framepointers not in use)

    SUBQ    $5, (SP)    // return to CALL again

    MOVQ    0(DX), BX

    JMP    BX    // but first run the deferred function

不喜欢看汇编代码(囧),从网上的解释来看其中BX就是deferreturn函数的参数地址,BX-8刚好就是刚好就是deferreturn的返回地址。(CALL会执行PUSH IP), SUBQ $5, (SP)减去5使得SP的返回地址刚好减掉了CALL runtime.deferreturn(SB)这个指令的长度。所以在执行最后的JMP BX之前,SP的指令就是刚好就是CALL runtime.deferreturn(SB)的地址。
当执行完成以后,执行RET指令,从SP取出返回地址,又重新执行CALL runtime.deferreturn(SB),从而形成递归调用。

有人画了一个图片来说明:
image.png

【上面这一段太绕了,无视就好,只需要理解jumpdefer最终会把当前g上面的defer都执行一遍】

通过Goexit退出goroutine

因为defer是绑定到对应的goroutineg对象上,如果强制退出goroutine,那么将会一次将g上面所有的defer对象进行调用:

func Goexit() {

    // Run all deferred functions for the current goroutine.

    // This code is similar to gopanic, see that implementation

    // for detailed comments.

    gp := getg()

    for {

        d := gp._defer

        if d == nil {

            break

        }

        if d.started {

            if d._panic != nil {

                d._panic.aborted = true

                d._panic = nil

            }

            d.fn = nil

            gp._defer = d.link

            freedefer(d)

            continue

        }

        d.started = true

 

        //调用defer函数

        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

        if gp._defer != d {

            throw("bad defer entry in Goexit")

        }

        d._panic = nil

        d.fn = nil

        gp._defer = d.link

        freedefer(d)

        // Note: we ignore recovers here because Goexit isn't a panic

    }

    goexit1()

}

defer使用的场景

不要在defer中修改返回值

func add(a, b int) int {

    ret := 0

 

    defer func() {

        ret = 100

    }()

 

    ret = a + b

    return ret

}

 

func main()  {

    r := add(2, 3)

    fmt.Println(r)

}

上面代码本来的意图是希望返回100,但是实际结果却是返回5,那么实际原因是什么呢,我们看一下汇编代码:

MOVQ    "".b+56(FP), AX

MOVQ    "".a+48(FP), CX

ADDQ    CX, AX

MOVQ    AX, "".ret+24(SP)

MOVQ    AX, "".~r2+64(FP)

PCDATA    $0, $0

XCHGL    AX, AX

CALL    runtime.deferreturn(SB)

MOVQ    32(SP), BP

ADDQ    $40, SP

RET

从上面的汇编代码可以看出,return ret这个语句并不是原子操作,被分成了两个部分:

MOVQ    AX, "".~r2+64(FP)  //给函数返回值赋值

CALL    runtime.deferreturn(SB)  //中间插入了defer的调用

RET  //函数返回

所以在deferreturn调用的时候,已经给返回值赋值了,后面的修改都是修改局部变量。

所以这里建议,defer不要做返回值修改这种操作,就是用来进行资源回收或者异常处理的。比如文件的OpenClose, 锁的LockUnlock,这个和Java里面的try-finally的效果是一样的。

有些资源释放直接释放就行,不是什么都需要defer

defer可以在函数退出的时候直接调用执行,在里面释放资源是最简单的,但是有的时候这个函数可能必须提前关闭资源,因为其他的子函数需要重新使用这个资源

func writeFile(tmpName string, fileName string) error {

    fd, err := os.OpenFile(tmpFileName, os.O_RDWR)

    if err != nil {

        return err

    }

 

    defer fd.Close()  //这里使用defer关闭就不太合适,因为后面Rename函数需要重新使用这个资源,最好在Rename调用之前就执行Close

 

    //操作fd,进行写操作

    fd.Write(b)

 

    err = os.Rename(tmpName, fileName)

    if err != nil {

        return nil

    }

 

    return nil

}

对于下这两段代码,明显直接使用test1更合适,这样性能更高,不需要defer那一堆操作和数据结构的申请。

func test1() {

    lock.Lock()

    x = x + 1

    lock.Unlock()

}

 

 

func test2() {

    lock.Lock()

    defer lock.Unlock()

 

    x = x + 1

}

注意defer的顺序

如果一个函数里面有多个defer,那么defer调用的顺序类似于栈的顺序,先进后出

func test() {

  defer fmt.Println(1)

  defer fmt.Println(2)

  defer fmt.Println(3)

}

打印的顺序3, 2, 1


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。