golang-notes Go source code analysis(zh-cn) 项目地址: https://gitcode.com/gh_mirrors/go/golang-notes
在Go语言中,defer语句是一种非常实用的特性,它允许我们在函数返回前执行某些操作。本文将基于cch123/golang-notes项目中的defer实现分析,深入探讨defer的工作原理和底层实现机制。
defer语句用于注册延迟调用,这些调用会在函数返回前被逆序执行(后进先出)。这种特性在处理资源释放、锁释放等场景非常有用。
func example() {
f, err := os.Open("file.txt")
if err != nil {
return
}
defer f.Close() // 确保文件在函数返回前被关闭
// 其他操作...
}
当编译器遇到defer语句时,会将其转换为两个关键步骤:
runtime.deferproc
:注册defer函数runtime.deferreturn
:执行defer函数deferproc
函数负责创建一个_defer
结构体并将其挂载到当前goroutine的_defer
链表上。关键代码如下:
func deferproc(siz int32, fn *funcval) {
// 获取调用者的栈指针和程序计数器
sp := getcallersp(unsafe.Pointer(&siz))
callerpc := getcallerpc()
// 创建新的defer结构体
d := newdefer(siz)
d.fn = fn
d.pc = callerpc
d.sp = sp
// 处理参数
// ...(参数拷贝逻辑)
return0()
}
_defer
结构体是defer机制的核心数据结构:
type _defer struct {
siz int32 // 参数总大小
started bool // 是否已开始执行
sp uintptr // 调用者栈指针
pc uintptr // 调用者程序计数器
fn *funcval // 函数信息
_panic *_panic // 关联的panic
link *_defer // 链表指针
}
这个结构体形成了一个链表,后注册的defer会放在链表头部,实现了LIFO(后进先出)的特性。
newdefer
负责分配和初始化_defer
结构体:
func newdefer(siz int32) *_defer {
// 尝试从P的本地缓存获取
// ...
// 缓存未命中则分配新的
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
// 初始化并链接到当前g的defer链表
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
deferreturn
负责执行defer函数:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
// 检查栈指针是否匹配
sp := getcallersp(unsafe.Pointer(&arg0))
if d.sp != sp {
return
}
// 拷贝参数
// ...
// 准备执行
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
// 跳转到defer函数
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
jmpdefer
使用汇编实现,其核心技巧是修改返回地址,使得deferreturn
被反复调用:
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // 函数地址
MOVQ argp+8(FP), BX // 调用者sp
LEAQ -8(BX), SP // 调整sp
MOVQ -8(SP), BP // 恢复BP
SUBQ $5, (SP) // 修改返回地址
MOVQ 0(DX), BX
JMP BX // 跳转到defer函数
通过将返回地址减5(call指令的长度),使得defer函数返回后会再次执行deferreturn
,直到所有defer都被执行完毕。
_defer
结构体在性能敏感的场景,可以考虑避免在循环中使用defer,或者预分配_defer
结构体。
defer机制与panic/recover紧密相关。当发生panic时,运行时系统会遍历当前goroutine的defer链表,并在执行defer函数时检查是否有recover调用。
Q: 为什么需要多次调用deferreturn?
A: 从实现角度看,deferproc
和deferreturn
成对出现使得编译器实现更简单。虽然jmpdefer已经可以实现循环执行,但保持这种对称性有助于代码的清晰性和可维护性。
Q: defer函数的参数何时确定?
A: defer函数的参数在注册时(即执行defer语句时)就确定了,而不是在执行时才求值。这一点对于理解defer的行为非常重要。
通过分析cch123/golang-notes中的defer实现,我们深入了解了Go语言defer机制的内部工作原理。defer通过_defer
链表和巧妙的汇编实现,提供了强大的延迟执行功能。理解这些底层细节有助于我们更好地使用defer,并能在性能优化时做出更明智的决策。
golang-notes Go source code analysis(zh-cn) 项目地址: https://gitcode.com/gh_mirrors/go/golang-notes