defer 修饰的函数是一个延迟函数,在包含它的函数返回时运行。
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking
defer 触发时机是:
defer 数据结构源码在 src/runtime/runtime2.go
在这里插入图片描述
type _defer struct {
siz int32 // 由deferproc第一个参数传入,参数和结果的内存大小
started bool // 标识defer函数是否已经开始执行
heap bool // 堆分配、栈分配
openDefer bool //表示当前 defer 是否经过开放编码的优化
sp uintptr // 栈指针程序计数器,注册defer函数的函数栈指针
pc uintptr // 调用方程序计数器,deferproc函数返回后要继续执行的指令地址
fn *funcval // 由deferproc的第二个参数传入,也就是被注册的defer函数
_panic *_panic // 是触发defer函数执行的panic指针,正常流程执行defer时它就是nil
link *_defer //结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。
}
在中间代码生成阶段, 有三种不同的机制处理 defer 关键字
defer
语句堆位置插入 runtime.deferproc
, 在被执行时,延迟调用会被保存为一个 _defer
记录,并将被延迟调用的入口地址与参数复制保存,存入 Gorountine
得调用链表中。runtime.deferreturn
,当被执行时,会将延迟调用从 Goroutine
链表中取出并执行,多个延迟调用则以 jmpdefer
尾递归调用方式连续执行runtime.deferproc
负责注册, runtime.deferreturn
负责执行。
在这里插入图片描述
cmd/compile/internal/gc.state.call
会负责为所有函数和方法调用生成中间代码:
cmd/compile/internal/gc.state.newValue1A
函数生成函数调用的中间代码defer
, 那么会单独生成相关的结束代码块。// Calls the function n using the specified call type.
// Returns the address of the return value (or nil if none).
func (s *state) call(n *Node, k callKind) *ssa.Value {
var call *ssa.Value
if k == callDeferStack {
// 在栈上初始化 defer 结构体
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem())
...
} else {
...
switch {
case k == callDefer:
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem())
...
}
call.AuxInt = stksize
}
s.vars[&memVar] = call
...
}
runtime.deferproc
的功能时注册延迟函数,会为 defer
创建一个新的 runtime._defer
的结构体、设置它的函数指针 fn
、程序计数器 pc
和 栈制作 sp
并将相关的函数参数拷贝到相邻的内存空间。
runtime.funcval
结构体的指针link
字段上形成链表。runtime.return0
是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn
的递归调用。func deferproc(siz int32, fn *funcval) {
// 这里的g就是gorouutine,详看golang的调度模型
// 只有用户使用的goroutine可以应用defer
if getg().m.curg != getg() {
throw("defer on system stack")
}
sp := getcallersp() // 调用deferproc之前的rsp寄存器的值
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc() // deferproc函数的返回地址,也就是执行完defer后应当跳回哪段代码上
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
// 对defer函数参数进行处理
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()
}
先通过 newdefer 获取一个 defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的,后调用的 defer 会优先执行。
newdefer追加新等延迟调用
runtime.deferreturn
是触发延迟函数链表的执行,会从 Goroutine
的 _defer
链表中取出最前面的 runtime._defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数。
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
// 结束条件1,没有defer函数了,也就是所有defer函数都执行完成了
// 还记得defer的链式结构吗,其实就是一个递归函数不断调用
// 为nil的话就代表这条链遍历完成了
return
}
sp := getcallersp()
if d.sp != sp {
// 结束条件2,如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回
// 因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如:
// a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数
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)))
}
包含如下几个步骤:
值得一提的是,defer具有即时传值的特点,defer也同样满足闭包和匿名函数的特性。
以代码为例子,讲解 defer 注册和执行流程
// 源程序
func A1(a int) {
fmt.Println(a)
}
func A() {
a, b := 1, 2
defer A1(a)
a = a + b
fmt.Println(a, b)
}
//函数A编译后的伪指令
func A() {
a, b := 1, 2
runtime.deferproc(8, A1,1) // siz=8, A1=延迟函数入口, 1=A1函数入参
a = a + b
fmt.Println(a, b)//3,2
runtime.deferreturn()//执行defer链表
return
}
deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。
在这里插入图片描述
频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲_defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。
在这里插入图片描述
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。