Golang脱俗的defer
-
浅谈defer
GO中的defer一直都是项目中常用的依赖。无论是解锁,还是关闭文件,或者关闭session,大多都离不开defer。稍微使用过golang的粉(huan)丝(zhe)应该都知道,程序即使panic,defer也依然会照常执行。但是与传统中逐行执行相比,defer会造成较大的开销,这也是被行业中所诟病的痛点。相较于讲how,我们不如谈谈why吧,相比于语法,我觉得我们大可讨论下实现原理,来增加些“驾驶乐趣”。
说之前,我们简要的看下defer的实现原理哈~。先上一段代码吧:
package main
func main() {
defer func() {
print("hello, I'm xyp")
}()
return
}
我们分别使用两个版本的Go进行编译(1.13和1.14)。
使用golang的反汇编工具进行反汇编:
go tool compile -S main.go
不处所料,汇编代码中对defer的引用以及return都有所不同。
1.13反汇编后的结果
0x0036 00054 (main.go:7) MOVL $0, (SP)
0x003d 00061 (main.go:7) LEAQ "".main.func1·f(SB), AX
0x0044 00068 (main.go:7) MOVQ AX, 8(SP)
0x0049 00073 (main.go:7) PCDATA $1, $0
0x0049 00073 (main.go:7) CALL runtime.deferproc(SB)
0x004e 00078 (main.go:7) TESTL AX, AX
0x0050 00080 (main.go:7) JNE 84
0x0052 00082 (main.go:7) JMP 34
0x0054 00084 (main.go:7) XCHGL AX, AX
1.14反汇编后的结果
0x004f 00079 (main.go:6) PCDATA $1, $-1
0x004f 00079 (main.go:6) PCDATA $0, $-2
0x004f 00079 (main.go:6) CALL runtime.morestack_noctxt(SB)
0x0054 00084 (main.go:6) PCDATA $0, $-1
0x0054 00084 (main.go:6) JMP 0
由此可见,1.13及之前的版本,defer都是在堆上分配,而1.14及之后的版本,defer分配在栈上。
-
再探究竟
Go语言中,每个goroutine都有自己的一个defer链表,而runtime.deferproc函数做的事情就是把defer函数及其参数添加到链表中,即我们所谓的注册。然后编译器还会在当前函数结尾处插入runtime.deferreturn的调用代码,后者会按照LIFO的顺序调用当前函数注册的所有defer函数。如果当前goroutine发生了panic,或者调用了runtime.Goexit,runtime会按照LIFO的顺序遍历整个defer链表逐一执行defer函数,直到某个defer函数完成了recover,或者最后程序退出。
deferproc的函数原型如下:
// 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
gp := getg()
if gp.m.curg != gp {
// 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)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.link = gp._defer
gp._defer = d
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.
}
该function有两个input, siz即为编译器自动添加, 而fn指向一个runtime.funcval结构,里面有defer函数的地址
假设一段code如下:
func f1() {
defer func() {
print("hello world")
}()
print("hello xyp")
}
编译器会将其转为伪代码大致如下
func f1() {
r := runtime.deferproc(16, f1_func1, 10, 20) // recover后,返回时r为1,否则为0
if r > 0 {
goto ret
}
print("hello xyp")
runtime.deferreturn()
return
ret:
runtime.deferreturn()
}
// f1的defer函数
func f1_func1() {
print("hello world")
}