对 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. 参考
- 《Go 语言学习笔记》
最后,我之前只在博客 http://www.legendtkl.com 和知乎上(知乎专栏:Golang Inside)上面写文章,现在开始在公众号(公众号:legendtkl)上面尝试一下,如果你觉得不错,或者之前看过,欢迎关注或者推荐给身边的人。谢谢。