【Free Style】深入Go语言模型(4):defer的底层实现与使用注意事项
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对象,封装调用函数的一些信息,然后把它加到goroutine的g对象的defer链表中。【关于goroutine的g数据结构后面再单独讲,这里就不展开了】
// 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),从而形成递归调用。
有人画了一个图片来说明:
【上面这一段太绕了,无视就好,只需要理解jumpdefer最终会把当前g上面的defer都执行一遍】
通过Goexit退出goroutine
因为defer是绑定到对应的goroutine的g对象上,如果强制退出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不要做返回值修改这种操作,就是用来进行资源回收或者异常处理的。比如文件的Open和Close, 锁的Lock和Unlock,这个和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
- 点赞
- 收藏
- 关注作者
评论(0)