深入理解 Go 语言 defer

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()
}
复制代码

查看汇编代码如下。

```bash
$ 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
复制代码

另外在 Go 语言的 func 声明中如果返回值变量显示声明,也就是 func foo() (ret int) {} 的时候,rval 就是 ret。这么上面的题目中对于的函数执行简单来说就是如下代码片段。但是 f3 涉及到另外一个知识点,也就是闭包。

//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. github.com/golang/go/b…
  2. github.com/golang/go/b…
  3. 《Go 语言学习笔记》

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值