和Java
的finally
类似,Go
也提供了defer
关键字用来做函数最后的收尾动作。虽然defer
的用法看似简单,但是不理解其原理,依然会踩到很多坑。同时recover()
也是需要通过和defer
关键字配合使用。因此,这里简单分析一下defer
的原理。知其然,也知其所以然。
defer 源码解析
关于defer
的使用介绍,在官网有一篇详细的文章介绍,其中问题提出了defer
的3点注意事项:
- A deferred function’s arguments are evaluated when the defer statement is evaluated.
- Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- Deferred functions may read and assign to the returning function’s named return values.
简单翻译过来就是:
defer
注册函数的参数的值在声明的时候就已经确定-
如果函数中包含多个
defer
,那么defer
的执行顺序是后进先出 -
defer
可能会读取或修改函数的命名返回值。
_defer
通过defer
关键字注册延迟函数之后,会生成一个_defer
结构体来记录defer
相关的信息。_defer
结构体主要包含以下字段:
type _defer struct {
siz int32 //func()需要分配的栈帧的大小
started bool
openDefer bool //是否符合使用开发编码的条件
sp uintptr //caller sp
pc uintptr //caller 程序计数器
fn *funcval//具体的函数的地址
_panic *_panic
link *_defer //下一个defer节点
}
_defer
结构体的内存分配包含两种:
- 在栈上分配,通过
runtime.deferprocStack()
注册 - 在堆上分配,通过
runtime.deferproc()
注册
一般来说,在栈上分配
defer
结构体比在堆上分配要快30%。同时,默认来说,
defer
是在栈上分配。当defer
在for
循环中时,defer
会在堆上分配,因此这里需要注意别再for
循环中使用defer
。
接下来看第一个问题:defer
注册函数的参数的值在声明的时候就已经确定
比如下面的程序:
func deferTest() (r int) {
i := 9527
defer func(n int) {
fmt.Println(n)
}(i)
return
}
通过GOOS=darwin GOARCH=amd64 go tool compile -N -l -S main.go
编译得到汇编代码:
go version 1.16
TEXT "".deferTest(SB), ABIInternal, 104-8
MOVQ (TLS), CX
CMPQ SP, 16(CX)
PCDATA0, -2
JLS 130
PCDATA0, -1
SUBQ104, SP
MOVQ BP, 96(SP)
LEAQ 96(SP), BP
MOVQ 0, "".r+112(SP) //初始化返回值r
MOVQ9527, "".i+8(SP) //初始化局部变量 i
MOVL 8, ""..autotmp_2+16(SP) //设置defer结构体的varp值,func栈帧大小为8
LEAQ "".deferTest.func1·f(SB), AX MOVQ AX, ""..autotmp_2+40(SP) //设置defer结构的fn的值,对应func在全局中的地址
MOVQ "".i+8(SP), AX MOVQ AX, ""..autotmp_2+88(SP) //设置func的参数
LEAQ ""..autotmp_2+16(SP), AX
MOVQ AX, (SP)
PCDATA1, $0
CALL runtime.deferprocStack(SB) //调用deferprocStack注册defer
....
通过上面的汇编代码可以看到。在调用runtime.deferprocStack()
注册defer
之前,func
所需要的参数就已经保存在栈上。
可以理解为defer
的原理分为两步:
- 获取
defer
需要执行的指令、参数等数据,打包保存,通过deferproStack()
将defer
结构体注册保存在goroutine
上 - 在函数返回的时,遍历
goroutine
获取对应的defer
,执行。
因此,defer
对应的方法的参数,在注册时便已经确定。
主要注意的时候,由于
defer
一般会有闭包,而golang
对应闭包捕获的变量都是变量的指针,因此需要注意区分defer
保存的值类型和指针类型。两者产生的结果完全不一样。这也是为什么说
defer
可能修改命名返回值的原因,因此defer
捕获的是命名返回值的指针,因此defer
对返回值的改变会影响到真正的返回值。
runtime.deferprocStack()
接下来看第二个注意点:如果函数中包含多个defer
,那么defer
的执行顺序是后进先出
查看runtime.deferprocStack()
源码可以看到:
//go:nosplit
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")
}
if goexperiment.RegabiDefer && d.siz != 0 {
throw("defer with non-empty frame")
}
// 这里defer结构体的fn和siz已经在编译时通过已经赋值,通过上面的汇编可以验证。因此deferprocStack只需要初始化其他值即可。
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
// 以下代码相当于
// d.panic = nil
// d.fd = nil
// d.link = gp._defer
// gp._defer = d
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
//return0和return的区别在于return0不会触发defer的调用,避免无限递归
return0()
}
最需要关注的是以上两行代码:
d.link = gp._defer
gp._defer = d
假如在执行这两行代码之前,goroutine
已经注册了一个defer
:
执行之后会变成:
也就是说每个defer
在注册的时候都会把自己设置的和goroutine
对接的那一个。
这里也可以注意到
defer
是goroutine
级别的。也就是所有的defer
都只能在当前goroutine
执行。这也是为什么recover()
捕获不了子gouroutine
的panic
的原因。
runtime.deferreturn()
接下来再看defer
的执行:从上面的汇编代码可以看到defer
的执行是通过runtime.deferreturn
完成。
func deferreturn() {
gp := getg()
d := gp._defer
//如果没有defer结构体,则直接返回
if d == nil {
return
}
//获取调用deferreturn的函数的sp地址
sp := getcallersp()
//如果defer结构体的sp和调用deferreturn的sp地址不同,则说明此defer不属于当前函数,则直接返回
if d.sp != sp {
return
}
//如果是用开放链接,则直接调用runOpenDeferFrame运行
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}
//获取caller当前栈帧的arg0的位置
argp := getcallersp() + sys.MinFrameSize
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(argp)) = *(*uintptr)(deferArgs(d))
default:
//将栈上的参数拷贝到func参数地址上
memmove(unsafe.Pointer(argp), deferArgs(d), uintptr(d.siz))
}
//释放defer
fn := d.fn
d.fn = nil
//将defer连接的下一个defer赋值给goroutine
gp._defer = d.link
//释放defer,主要是归还从堆上分配的defer结构体
freedefer(d)
_ = fn.fn
//通过jmpdefer执行func语句。
jmpdefer(fn, argp)
}
可以看到deferreturn
主要是执行一些过滤和准备工作。
从deferreturn
的过滤可以看出来,虽然defer
是挂接在goroutine
上,但是goroutine
通过栈结构保存defer
结构体,在每次调用deferreturn
时,都会从当前goroutine
开始顺序遍历defer
结构体并执行,deferprocStack
和deferreturn
都是配对执行,deferprocStack
对应defer
结构体入栈,deferreturn
结构体对应defer
结构体出栈。
通过defer
结构体的入栈和出栈,可以对应到defer
的第二个注意点:如果函数中包含多个defer
,那么defer
的执行顺序是后进先出。
jmpdefer
这里简单分析一下jmpdefer
,前面说到每次执行deferreturn
时,都会遍历gouroutine
的defer
列表,可是看代码可以发现这里并没有for
循环,那defer
是如何遍历的呢?答案在jmpdefer
,找到jmpdefer
的声明和实现:
//第一个参数:需要执行的func()的全局地址
//第二个参数:
func jmpdefer(fv *funcval, argp uintptr)
其实现是直接由汇编实现:
TEXT runtime·jmpdefer(SB), NOSPLIT, 0-16
MOVQ fv+0(FP), DX //将fn的全局地址保存在DX寄存器中
MOVQ argp+8(FP), BX //将caller sp的地址保存在BX寄存器中
LEAQ -8(BX), SP //获取caller sp寄存器中 保存的地址上对于的值-8 保存在sp寄存器中 MOVQ -8(SP), BP //保存caller BP
SUBQ5, (SP) //将SP寄存器中的值减5
MOVQ 0(DX), BX //将fn的地址赋值给BX
JMP BX //跳转到fn的地址执行fn
这里的奥秘就在于func()
不是通过CALL
指令调用,而是通过JMP
调用。
回忆Go汇编(一)提到过,CALL
指令的作用主要是将下一条需要执行的指令的地址POP
到栈上作为return address
,然后修改IP
寄存器实现CPU
跳转。RET
指令主要是通过读取栈上的return address
实现函数的返回。这里使用JMP
是因为return address
是需要程序自己设置,只要将return address
的地址设置为deferreturn
函数的地址,便形成了一个递归。
继续说,那么deferreturn
的地址是如何获取的呢?前面说过,CALL
指令保存了CALL deferreturn()
指令的下一条地址。那么我们只需要获取CALL
指令保存的地址,再将地址减去CALL
指令的指令长度,即可获取CALL deferreturn
的地址。
jmpdefer
的第二个参数的设置在deferreturn()
:
//获取caller当前栈帧的sp寄存器的值+一个最小栈帧的大小
//这里的sp都是硬件sp
argp := getcallersp() + sys.MinFrameSize
此时的函数调用栈如图所示:
因此,caller sp
减去一个帧的大小,此时argp
指向arg0
的位置。通过下面几条指令的计算:
MOVQ argp+8(FP), BX
LEAQ -8(BX), SP //获取return address
SUBQ $5, (SP) //将return address - 5 的值设置为func()的return address
可以看到,经过系列的计算,从而实现了deferreturn
递归执行的效果。
defer和返回值
func testA()(r int){
defer func() {
r++
}()
return
}
func testB() int {
i := 1
defer func() {
i++
}()
return i
}
观察testA()
和testB()
的返回结果。这里不再进行原理分析,而是分析一下其执行流程。想要理解其差别,只需要记住一点即可:返回值的会提前在栈上分配内存。因此,对于testA()
,流程为:
- 分配返回值内存
r
,并将其初始化为0 //这里其实并不严谨,因为栈的内存都是一次性分配完毕的,这里为了方便理解。下同 - 执行
defer
defer
捕获r
的地址defer
修改r
的值,执行r=r+1
- 返回
r
因为这里是闭包捕获,因此defer
中r
的值的修改会影响到真正的返回结果。
对于testB()
,其流程为:
- 分配返回值内存
r
,并将其初始化为0 - 分配局部变量
i
的内存,并将其初始化为1 - 执行
r=i
- 执行
defer
- 返回
r
因此,defer
捕获的仅仅是i
的地址,i++
完全不会影响返回值r
,其等价的结果如下:
func testB()(r int) {
i := 1
defer func() {
i++
}()
r = i
return r
}
参考文章: