简介
延迟调用(defer)的优势是:
- 即使函数执行出错,依然能保证回收资源等操作得以执行
- 可以在变量的定义处加入defer,代码结构上避免忘记做某些数据的回收
劣势:
- 性能上会会比直接调用慢一些
- 如果在defer中释放,相对来说只会在函数执行结束的时候才会调用,变量生命周期会变长.
定义
编写以下程序, dump出汇编.
defer主要调用了一下两个函数func deferprocStack(d *_defer)和func deferreturn(arg0 uintptr)
package main import ( "fmt" ) func main() { defer fmt.Println(0x11) } (base) ➜ readsrc go tool objdump -s "main\.main" ./test TEXT main.main(SB) /home/darcyaf/Development/go/src/readsrc/main.go main.go:7 0x48cf30 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX main.go:7 0x48cf39 488d4424d8 LEAQ -0x28(SP), AX main.go:7 0x48cf3e 483b4110 CMPQ 0x10(CX), AX main.go:7 0x48cf42 0f86b1000000 JBE 0x48cff9 main.go:7 0x48cf48 4881eca8000000 SUBQ $0xa8, SP main.go:7 0x48cf4f 4889ac24a0000000 MOVQ BP, 0xa0(SP) main.go:7 0x48cf57 488dac24a0000000 LEAQ 0xa0(SP), BP main.go:8 0x48cf5f 0f57c0 XORPS X0, X0 main.go:8 0x48cf62 0f11842490000000 MOVUPS X0, 0x90(SP) main.go:8 0x48cf6a 488d050f190100 LEAQ 0x1190f(IP), AX main.go:8 0x48cf71 4889842490000000 MOVQ AX, 0x90(SP) main.go:8 0x48cf79 488d05a0cd0400 LEAQ 0x4cda0(IP), AX main.go:8 0x48cf80 4889842498000000 MOVQ AX, 0x98(SP) main.go:8 0x48cf88 c744243030000000 MOVL $0x30, 0x30(SP) main.go:8 0x48cf90 488d0561c80300 LEAQ 0x3c861(IP), AX main.go:8 0x48cf97 4889442448 MOVQ AX, 0x48(SP) main.go:8 0x48cf9c 488d842490000000 LEAQ 0x90(SP), AX main.go:8 0x48cfa4 4889442460 MOVQ AX, 0x60(SP) main.go:8 0x48cfa9 48c744246801000000 MOVQ $0x1, 0x68(SP) main.go:8 0x48cfb2 48c744247001000000 MOVQ $0x1, 0x70(SP) main.go:8 0x48cfbb 488d442430 LEAQ 0x30(SP), AX main.go:8 0x48cfc0 48890424 MOVQ AX, 0(SP) main.go:8 0x48cfc4 e867b7f9ff CALL runtime.deferprocStack(SB) main.go:8 0x48cfc9 85c0 TESTL AX, AX main.go:8 0x48cfcb 7516 JNE 0x48cfe3 main.go:9 0x48cfcd 90 NOPL main.go:9 0x48cfce e85dbdf9ff CALL runtime.deferreturn(SB) main.go:9 0x48cfd3 488bac24a0000000 MOVQ 0xa0(SP), BP main.go:9 0x48cfdb 4881c4a8000000 ADDQ $0xa8, SP main.go:9 0x48cfe2 c3 RET main.go:8 0x48cfe3 90 NOPL main.go:8 0x48cfe4 e847bdf9ff CALL runtime.deferreturn(SB) main.go:8 0x48cfe9 488bac24a0000000 MOVQ 0xa0(SP), BP main.go:8 0x48cff1 4881c4a8000000 ADDQ $0xa8, SP main.go:8 0x48cff8 c3 RET main.go:7 0x48cff9 e8a247fcff CALL runtime.morestack_noctxt(SB) main.go:7 0x48cffe e92dffffff JMP main.main(SB) 在func deferprocStack(d *_defer), 这里将defer的函数调用全部放到g._defer上,串成一个链表等待调用,将新加入的defer调用放在前面,然后根据link去调用,这也能解释为什么越晚的defer越早调用.
func deferprocStack(d *_defer) { gp := getg() if gp.m.curg != gp { // go code on the system stack can't defer throw("defer on system stack") } // siz and fn are already set. // The other fields are junk on entry to deferprocStack and // are initialized here. d.started = false d.heap = false d.sp = getcallersp() d.pc = getcallerpc() // The lines below implement: // d.panic = nil // d.link = gp._defer // gp._defer = d *(*uintptr)(unsafe.Pointer(&d._panic)) = 0 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) (dlv) p gp._defer *runtime._defer { siz: 48, started: false, heap: false, sp: 824634305936, pc: 4771785, fn: *runtime.funcval {fn: 4745216}, _panic: *runtime._panic nil, link: *runtime._defer { siz: 48, started: false, heap: false, sp: 824634306112, pc: 4772144, fn: *(*runtime.funcval)(0x4c97f8), _panic: *runtime._panic nil, link: *(*runtime._defer)(0xc00008eed0),},} 前面都是遇到defer就将其加到gp._defer链表中,deferreturn才是真正执行的时候.
这里gp._defer = d.link相当于取出了最后一个defer, 然后调用jmpdefer执行串成了一个链表,怎么区分多个函数的defer呢,这里就通过sp指针,判断caller中sp指针和defer当时的sp指针来判断.
在这里调用了freedefer(d),会将当前d放到pp.deferpool中,类似于p.cache,是defer的本地缓存,当然如果本地缓存满了,会将pp.deferpool的数据放一半到sched.deferpool
runtime.jmpdefer中,b
func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { return } sp := getcallersp() 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) jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) } 在jmpdefer中,当执行完后,
// void jmpdefer(fv, sp); // called from deferreturn. // 1. grab stored LR for caller // 2. sub 4 bytes to get back to BL deferreturn // 3. BR to fn (base) ➜ readsrc go tool objdump -s "runtime.jmpdefer" ./test TEXT runtime.jmpdefer(SB) /usr/local/go/src/runtime/asm_amd64.s asm_amd64.s:587 0x452dc0 488b542408 MOVQ 0x8(SP), DX// 第一个参数,fn地址 asm_amd64.s:588 0x452dc5 488b5c2410 MOVQ 0x10(SP), BX // 第二个参数arg0 asm_amd64.s:589 0x452dca 488d63f8 LEAQ -0x8(BX), SP //call deferreturn时压入的caller IP指针 asm_amd64.s:590 0x452dce 488b6c24f8 MOVQ -0x8(SP), BP // call的上一个地址,改为基址 asm_amd64.s:591 0x452dd3 48832c2405 SUBQ $0x5, 0(SP) //减去call指令,即下一次要执行call deferreturn asm_amd64.s:592 0x452dd8 488b1a MOVQ 0(DX), BX // 压入fn函数 asm_amd64.s:593 0x452ddb ffe3 JMP BX 跳转到fn函数执行,用JMP而不是CALL,因为是同一个函数里面 如果中途调用goexit终止,他会负责处理整个调用堆栈的延迟函数
func Goexit() { 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 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 } } 性能
延迟调用远不是一个call指令那么简单,会涉及到对象分配,缓存和多次函数调用。 在性能要求比较高的场合,应该避免使用defer,go1.13测试的时候有3x的性能差距
BenchmarkNormal-12 100000000 11.2 ns/op BenchmarkDefer-12 37844540 31.1 ns/op panic
panic和defer的实现类似,也是放在gp._panic上面
如果recovered,那么会调用recover,recover会调用gogo(&gp.sched),否则defer结束后打印panic
func gopanic(e interface{}) { var p _panic p.arg = e p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) for { d := gp._defer if d == nil { break } if d.started { //如果已经开始了,则执行下一个 continue } if p.recovered { //如果defer中执行了recovered, mcall(recovery) //调用recover继续执行 throw("recovery failed") // mcall should not return } preprintpanics(gp._panic) fatalpanic(gp._panic) // should not return } recover的实现是gorecover
调用后,判断gp._panic
如果不为nil, 且不是recovered状态,那么设置其p.recovered=true,改为已恢复状态
注意: 这里也通过p.argp指针和当前的调用指针比较来区分不同函数的panic。
func gorecover(argp uintptr) interface{} { // Must be in a function running as part of a deferred call during the panic. // Must be called from the topmost function of the call // (the function used in the defer statement). // p.argp is the argument pointer of that topmost deferred function call. // Compare against argp reported by caller. // If they match, the caller is the one who can recover. gp := getg() p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil } 有疑问加站长微信联系(非本文作者)
