对 Golang 感兴趣的同学欢迎关注公众号:golang-experts

defer 估计是每个 Gopher 每天写代码都会写,那么你是不是真正的理解了 defer 呢?不妨看一下下面这个代码片段,这个是我之前给 UC 那边一个 team 做 Golang 培训的时候想的例子。

package main

func f() int {
    i := 5
    defer func() {
        i++
    }()
    return i
}

func f1() (result int) { 
    defer func() { 
        result++ 
    }() 
    return 0
}

func f2() (r int) { 
    t := 5 
    defer func() { 
        t = t + 5 
    }()
    return t
}

func f3() (r int) { 
    defer func(r int) { 
        r = r + 5 
    }(r) 
    return 1
}

func main() {
    println(f())
    println(f1())
    println(f2())
    println(f3())
}

1. return 语句

在解析上面的题目之前,要理解一个前提是 Go 的函数返回值是通过堆栈返回的,这也是实现了多返回值的方法。举个例子。

//foo.go
package main

func foo() (int,int){
   i := 1
   j := 2
   return i,j
}

func main() {
    foo()
}

查看汇编代码如下。

$ go build -gcflags '-l' -o foo foo.go
$ go tool objdump -s "main\.foo" foo
TEXT main.foo(SB) /Users/kltao/code/go/src/example/foo.go
  bar.go:6      0x104ea70       48c744240801000000  MOVQ $0x1, 0x8(SP)
  bar.go:6      0x104ea79       48c744241002000000  MOVQ $0x2, 0x10(SP)
  bar.go:6      0x104ea82       c3          RET

也就是说 return 语句不是原子操作,而是被拆成了两步

rval = xxx
ret

而 defer 语句就是在这两条语句之间执行,也就是

rval = xxx
defer_func
ret
func foo() (ret int) {}
//f
rval = i
i ++ 
ret

//f1
result = 0
defer // result ++
return

//f2
r = t
defer // t = t + 5
return

2. 闭包

简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据,这样就会产生一种结果,虽然数据定义是在函数外,但是在函数内部操作数据也会对数据产生影响。如下面的例子所示,foo() 中的匿名函数对 i 的调用就是闭包引用,i++ 会影响外面定义的 i 的值。而 bar() 中的匿名函数是变量拷贝,i++ 并不会修改外部 i 值。这么看的话,开始的 f3() 的输出你是不是知道是多少了呢?

func foo() {
    i := 1
    go func() {
       i ++ 
    }()
    time.Sleep(xxx)
    println(i)
}

func bar() {
    i := 1
    go func(i int) {
        i ++
    }(i)
    time.Sleep(xxx)
    println(i)
}

3. defer 的使用场景

在我最开始学习 Go 语言的时候,我看到 defer 的第一反应就是 Python 中的如下语句。也就是说不用显示地关闭文件句柄,除此之外还有网络连接等各种资源都可以放到 defer 里面来释放。

with open("file", "a") as f:
    // handler

但是随着写代码越来越多,我觉得上面说的这些场景如果明确知道什么时候要释放资源,那么都不是非使用 defer 不可的,因为使用 defer 还是有很大开销的,下面说。使用 defer 的最合适的场景我觉得应该是和 recover 结合使用,也就是说在你不知道的程序何时可能会 panic 的时候,才引入 defer + recover。

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
}

4. defer 的底层实现

defer 的底层实现主要由两个函数:

  • func deferproc(siz int32, fn *funcval)
  • func deferreturn(arg0 uintptr)

看代码。下面的代码执行了两次 defer ,defer 的执行是按 FILO 的次序执行的,也就是说下面代码的输出是

world
hello2
hello1

这个就不细说了。看汇编代码。

package main

import (
    "fmt"
)

func main() {
    defer    fmt.Println("hello1")
    defer    fmt.Println("hello2")

    fmt.Println("world")
}

编译,objdump。

$ go build -gcflags '-l' -o defer defer.go
$ go tool objdump -s "main\.main" defer
TEXT main.main(SB) /Users/kltao/code/go/src/example/defer2.go
  ...
  defer2.go:8       0x1092fe1       0f57c0          XORPS X0, X0
  defer2.go:8       0x1092fe4       0f11442450      MOVUPS X0, 0x50(SP)
  defer2.go:8       0x1092fe9       488d05100c0100      LEAQ type.*+68224(SB), AX
  defer2.go:8       0x1092ff0       4889442450      MOVQ AX, 0x50(SP)
  defer2.go:8       0x1092ff5       488d0db4b00400      LEAQ main.statictmp_0(SB), CX
  defer2.go:8       0x1092ffc       48894c2458      MOVQ CX, 0x58(SP)
  defer2.go:8       0x1093001       c7042430000000      MOVL $0x30, 0(SP)
  defer2.go:8       0x1093008       488d0d999d0300      LEAQ go.func.*+8(SB), CX
  defer2.go:8       0x109300f       48894c2408      MOVQ CX, 0x8(SP)
  defer2.go:8       0x1093014       488d542450      LEAQ 0x50(SP), DX
  defer2.go:8       0x1093019       4889542410      MOVQ DX, 0x10(SP)
  defer2.go:8       0x109301e       48c744241801000000  MOVQ $0x1, 0x18(SP)
  defer2.go:8       0x1093027       48c744242001000000  MOVQ $0x1, 0x20(SP)
  defer2.go:8       0x1093030       e81b3bf9ff      CALL runtime.deferproc(SB)
  defer2.go:8       0x1093035       85c0            TESTL AX, AX
  defer2.go:8       0x1093037       0f85b8000000        JNE 0x10930f5
  defer2.go:9       0x109303d       0f57c0          XORPS X0, X0
  defer2.go:9       0x1093040       0f11442440      MOVUPS X0, 0x40(SP)
  defer2.go:9       0x1093045       488d05b40b0100      LEAQ type.*+68224(SB), AX
  defer2.go:9       0x109304c       4889442440      MOVQ AX, 0x40(SP)
  defer2.go:9       0x1093051       488d0d68b00400      LEAQ main.statictmp_1(SB), CX
  defer2.go:9       0x1093058       48894c2448      MOVQ CX, 0x48(SP)
  defer2.go:9       0x109305d       c7042430000000      MOVL $0x30, 0(SP)
  defer2.go:9       0x1093064       488d0d3d9d0300      LEAQ go.func.*+8(SB), CX
  defer2.go:9       0x109306b       48894c2408      MOVQ CX, 0x8(SP)
  defer2.go:9       0x1093070       488d4c2440      LEAQ 0x40(SP), CX
  defer2.go:9       0x1093075       48894c2410      MOVQ CX, 0x10(SP)
  defer2.go:9       0x109307a       48c744241801000000  MOVQ $0x1, 0x18(SP)
  defer2.go:9       0x1093083       48c744242001000000  MOVQ $0x1, 0x20(SP)
  defer2.go:9       0x109308c       e8bf3af9ff      CALL runtime.deferproc(SB)
  defer2.go:9       0x1093091       85c0            TESTL AX, AX
  defer2.go:9       0x1093093       7550            JNE 0x10930e5
  defer2.go:11      0x1093095       0f57c0          XORPS X0, X0
  defer2.go:11      0x1093098       0f11442460      MOVUPS X0, 0x60(SP)
  defer2.go:11      0x109309d       488d055c0b0100      LEAQ type.*+68224(SB), AX
  defer2.go:11      0x10930a4       4889442460      MOVQ AX, 0x60(SP)
  defer2.go:11      0x10930a9       488d0520b00400      LEAQ main.statictmp_2(SB), AX
  defer2.go:11      0x10930b0       4889442468      MOVQ AX, 0x68(SP)
  defer2.go:11      0x10930b5       488d442460      LEAQ 0x60(SP), AX
  defer2.go:11      0x10930ba       48890424        MOVQ AX, 0(SP)
  defer2.go:11      0x10930be       48c744240801000000  MOVQ $0x1, 0x8(SP)
  defer2.go:11      0x10930c7       48c744241001000000  MOVQ $0x1, 0x10(SP)
  defer2.go:11      0x10930d0       e80b99ffff      CALL fmt.Println(SB)
  defer2.go:12      0x10930d5       90          NOPL
  defer2.go:12      0x10930d6       e80543f9ff      CALL runtime.deferreturn(SB)
  defer2.go:12      0x10930db       488b6c2470      MOVQ 0x70(SP), BP
  defer2.go:12      0x10930e0       4883c478        ADDQ $0x78, SP
  defer2.go:12      0x10930e4       c3          RET
  defer2.go:9       0x10930e5       90          NOPL
  defer2.go:9       0x10930e6       e8f542f9ff      CALL runtime.deferreturn(SB)
  defer2.go:9       0x10930eb       488b6c2470      MOVQ 0x70(SP), BP
  defer2.go:9       0x10930f0       4883c478        ADDQ $0x78, SP
  defer2.go:9       0x10930f4       c3          RET
  ...

结合代码看,代码中使用了两次 defer,调用了 deferproc 和 deferreturn ,都是匹配成对调用的。我们看一下 Golang 源码里面对 deferproc 和 deferreturn 的实现。

// 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() {        // getg 是获取当前的 goroutine
        // 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()
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    callerpc := getcallerpc()

    d := newdefer(siz)  // 申请一个结构体用来存放 defer 相关数据
    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.
}

光看 deferproc 的代码只能看到一个申请 defer 对象的过程,并没有看到这个 defer 对象存储在哪里?那么不妨大胆设想一下,defer 对象是以链表的形式关联到 goroutine 上的。我们看一下 deferproc 中调用的 newdefer 函数。

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()
        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
            // Take the slow path on the system stack so
            // we don't grow newdefer's stack.
            systemstack(func() {
                lock(&sched.deferlock)
                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }
                unlock(&sched.deferlock)
            })
        }
        if n := len(pp.deferpool[sc]); n > 0 {
            d = pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
        }
    }
    if d == nil {
        // Allocate new defer+args.
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
        if debugCachedWork {
            // Duplicate the tail below so if there's a
            // crash in checkPut we can tell if d was just
            // allocated or came from the pool.
            d.siz = siz
            d.link = gp._defer
            gp._defer = d
            return d
        }
    }
    d.siz = siz
    d.link = gp._defer
    gp._defer = d
    return d
}

重点看第 44,45 行,gp 是当前的 goroutine,有一个字段 _defer 是用来存放 defer 结构的,然后我们发现 defer 结构有一个 link 字段其实就相当于链表指针。如果熟悉链表操作的话,第 44,45 行结合起来看就是将新的 defer 对象插入到 goroutine 关联的 defer 链表的头部。那么执行的时候就从头执行 defer 就是 FILO 的顺序了,deferreturn 的源码大家自己去看吧。

5. benchmark

看了第 4 部分,我们应该知道 defer 的调用开销相比直接的函数调用确实多了不少,那么有没有 benchmark 来直观的看一下呢?有的。这里使用雨痕的 《Go 语言学习笔记》的 benchmark 程序。

package main

import (
    "testing"
    "sync"
)

var m sync.Mutex

func call() {
    m.Lock()
    m.Unlock()
}

func deferCall() {
    m.Lock()
    defer m.Unlock()
}

func BenchmarkCall(b *testing.B) {
    for i:=0; i<b.N; i++ {
        call()
    }
}

func BenchmarkDeferCall(b *testing.B) {
    for i:=0; i<b.N; i++ {
        deferCall()
    }
}

测试结果如下,看的出来差距还是挺大的。

➜  df go test -bench=.
goos: darwin
goarch: amd64
pkg: example/df
BenchmarkCall-8         100000000           17.8 ns/op
BenchmarkDeferCall-8    20000000            56.3 ns/op

6. 参考

  1. 《Go 语言学习笔记》

最后,我之前只在博客 http://www.legendtkl.com 和知乎上(知乎专栏:Golang Inside)上面写文章,现在开始在公众号(公众号:legendtkl)上面尝试一下,如果你觉得不错,或者之前看过,欢迎关注或者推荐给身边的人。谢谢。