枯藤老树昏鸦,小桥流水人家

一、Go defer 底层实现核心逻辑
Go 中 defer 不是简单的 "延迟执行" 语法糖,而是由编译器 + 运行时(runtime) 共同实现的机制,核心逻辑可概括为:
- 编译期:编译器将 defer 语句转换成 runtime.deferproc 调用(登记延迟函数);
- 运行期:函数退出前触发 runtime.deferreturn 调用,按「后进先出」执行登记的延迟函数;
- 数据结构:每个 goroutine 维护一个 defer 链表,每个 defer 节点存储函数指针、参数、链接等信息。
二、Go 源码核心结构体与函数(基于 Go 1.21 版本,关键注解)
1. defer 核心结构体(runtime/runtime2.go)
go
// _defer 是 defer 链表的节点,存储单个延迟函数的所有信息
type _defer struct {
siz int32 // 延迟函数参数的总字节数
started bool // 标记该defer是否已经开始执行
heap bool // 标记该defer是分配在堆上(栈空间不足时)还是栈上
openDefer bool // 标记是否是开放编码的defer(编译器优化)
sp uintptr // 该defer创建时的栈指针(用于栈帧校验)
pc uintptr // 该defer创建时的程序计数器(返回地址)
fn func() // 要延迟执行的函数(已绑定参数)
_panic *_panic // 关联的panic信息(panic时defer执行用)
link *_defer // 指向下一个defer节点(形成链表,后进先出)
// 以下是参数存储区(内存布局上紧跟结构体)
// argp 指向参数的起始地址,args 是具体参数值
argp uintptr
args unsafe.Pointer
}
// g 是 goroutine 的核心结构体,每个goroutine都有自己的defer链表
type g struct {
// ... 其他字段省略 ...
_defer *_defer // 指向当前goroutine的defer链表头(最新登记的defer)
// ... 其他字段省略 ...
}
注解:
- _defer 是 defer 的核心载体,每个 defer 语句对应一个 _defer 节点;
- goroutine 的 _defer 字段指向链表头,新登记的 defer 会插入链表头部(保证后进先出);
- siz/argp/args 用于存储 defer 函数的参数(参数在登记时就拷贝到这里,所以参数值是 "快照")。
2. defer 登记函数:runtime.deferproc(runtime/defer.go)
go
// deferproc 由编译器插入,用于登记延迟函数
// 参数:siz是参数总字节数,fn是延迟函数,后续是函数参数
// 返回值:0表示登记成功,非0表示需要立即执行(特殊场景)
func deferproc(siz int32, fn func()) int32 {
// 1. 获取当前goroutine(g)
gp := getg()
if gp.m.curg != gp {
// 不在当前执行的goroutine中,禁止defer(避免竞态)
throw("defer on system stack")
}
// 2. 分配_defer节点(优先栈分配,栈空间不足则堆分配)
d := newdefer(siz)
if d == nil {
throw("newdefer failed")
}
// 3. 填充_defer节点核心字段
d.fn = fn // 绑定延迟函数
d.siz = siz // 参数总字节数
d.sp = getcallersp() // 记录当前栈指针
d.pc = getcallerpc() // 记录当前程序计数器
// 4. 拷贝defer函数的参数到_defer节点(关键!参数值在此时固定)
// argp指向调用deferproc时的参数起始地址
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
d.argp = argp
if siz > 0 {
// 把参数从当前栈拷贝到_defer节点的args区域
memmove(d.args, unsafe.Pointer(argp), uintptr(siz))
}
// 5. 将新的_defer节点插入goroutine的defer链表头部
d.link = gp._defer
gp._defer = d
// 6. 返回0,告诉编译器继续执行后续代码
return 0
}
// newdefer 分配_defer节点(优先栈,其次堆)
func newdefer(siz int32) *_defer {
gp := getg()
// 尝试从goroutine的栈上分配(栈分配更快,无GC开销)
if siz <= maxStackDeferSize {
// 栈上有足够空间,直接分配
d := gp._deferStack
// ... 栈分配逻辑省略 ...
return d
}
// 栈空间不足,从堆上分配(会标记heap=true)
d := (*_defer)(mallocgc(totalSize, deferType, true))
d.heap = true
return d
}
注解:
- deferproc 是编译器替我们调用的:写 defer fmt.Println("hello") 时,编译器会转换成 deferproc(参数大小, fmt.Println, "hello");
- 参数拷贝是核心:memmove 把参数值从当前栈拷贝到 _defer 节点,所以 defer 函数的参数在登记时就固定(而非执行时);
- 链表插入规则:新节点插在链表头,保证执行时 "后进先出"。
3. defer 执行函数:runtime.deferreturn(runtime/defer.go)
go
// deferreturn 由编译器插入到函数退出前,执行所有登记的defer函数
// 参数:arg0是函数的返回值地址(用于defer修改命名返回值)
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return // 没有登记的defer,直接返回
}
// 校验栈帧:确保defer属于当前函数栈(避免跨栈执行)
sp := getcallersp()
if d.sp != sp {
return
}
// 1. 取出当前defer节点的函数和参数
fn := d.fn
args := d.args
siz := d.siz
// 2. 从链表中移除当前defer节点(执行完就释放)
gp._defer = d.link
// 3. 执行延迟函数(核心)
d.started = true
// 调用defer函数,传入之前拷贝的参数
reflectcall(nil, unsafe.Pointer(fn), args, siz, uintptr(arg0))
// 4. 释放_defer节点(栈分配的复用,堆分配的标记GC)
if d.heap {
// 堆分配的节点,交给GC回收
freeDefer(d)
} else {
// 栈分配的节点,放入复用池
gp._deferStack = d
}
// 5. 递归执行下一个defer节点(保证链表全部执行)
deferreturn(arg0)
}
注解:
- deferreturn 会被编译器插入到函数的所有退出路径(return/panic)前;
- reflectcall 是底层函数调用逻辑,负责把参数传给 defer 函数并执行;
- 递归执行:每次执行一个 defer 节点后,递归调用 deferreturn 处理下一个节点,直到链表为空;
- 命名返回值修改:arg0 是函数返回值的地址,defer 闭包可通过该地址修改返回值。
4. panic 时的 defer 执行(runtime/panic.go)
go
// gopanic 是panic的底层实现,会遍历defer链表执行
func gopanic(e interface{}) {
gp := getg()
// ... 其他逻辑省略 ...
// 遍历当前goroutine的defer链表
for {
d := gp._defer
if d == nil {
break // 没有更多defer,退出循环
}
// 标记defer已开始执行
d.started = true
// 执行defer函数
reflectcall(nil, unsafe.Pointer(d.fn), d.args, d.siz, 0)
// 移除当前defer节点
gp._defer = d.link
// 释放节点
freeDefer(d)
// 如果defer中调用了recover,终止panic
if gp._panic.recovered {
// ... 恢复逻辑省略 ...
return
}
}
// ... 触发程序崩溃逻辑 ...
}
注解:panic 时会优先执行所有登记的 defer 函数,再判断是否有 recover,有则恢复,无则崩溃。
三、defer 执行流程可视化(Mermaid 流程图)
四、关键细节注解(源码之外的核心知识点)
1. 栈分配 vs 堆分配
- 大部分场景下,_defer 节点分配在 goroutine 的栈上(栈分配无 GC 开销,速度快);
- 当 defer 函数参数总字节数超过 maxStackDeferSize(默认~200 字节)时,会分配到堆上;
- 堆分配的 _defer 节点由 GC 回收,栈分配的节点会被复用(减少内存分配开销)。
2. 参数拷贝的底层原因
defer 函数的参数在 deferproc 阶段就拷贝到 _defer 节点,而非执行时读取,这就是为什么:
go
num := 10
defer fmt.Println(num) // 登记时拷贝num=10
num = 20 // 后续修改不影响
// 执行时输出10
3. 开放编码优化(openDefer)
Go 1.14+ 引入了 openDefer 优化:对于简单的 defer 语句(无循环、无 panic),编译器会直接将 defer 函数内联到函数退出前,跳过 _defer 链表创建,大幅提升性能。
- openDefer 字段标记该 defer 是否启用此优化;
- 核心场景:单 defer、无 panic 的函数,比如 defer file.Close()。
4. defer 与返回值的交互
函数返回的底层流程:
kotlin
return val → 1. 将val赋值给返回值变量 → 2. 执行deferreturn → 3. 返回值变量给调用者
命名返回值能被 defer 修改的原因:返回值变量在函数栈上提前分配,defer 可通过 arg0 地址修改该变量。
五、总结
- 核心实现:defer 由 deferproc(登记)和 deferreturn(执行)两大函数支撑,基于 goroutine 私有的 _defer 链表实现;
- 参数规则:参数在登记时拷贝到 _defer 节点,执行时不再读取最新值;
- 执行顺序:新 defer 插入链表头,执行时后进先出,panic 时也会遍历执行;
- 性能优化:优先栈分配 _defer 节点,简单场景启用 openDefer 内联优化。
关键记忆点:defer 的 "登记 - 执行" 是链表 + 栈 / 堆分配的组合,参数拷贝是 "快照",执行是 "倒序",panic 时也能兜底执行。