go从零单排之defer源码

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

一、Go defer 底层实现核心逻辑

Go 中 defer 不是简单的 "延迟执行" 语法糖,而是由编译器 + 运行时(runtime) 共同实现的机制,核心逻辑可概括为:

  1. 编译期:编译器将 defer 语句转换成 runtime.deferproc 调用(登记延迟函数);
  1. 运行期:函数退出前触发 runtime.deferreturn 调用,按「后进先出」执行登记的延迟函数;
  1. 数据结构:每个 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 地址修改该变量。


五、总结

  1. 核心实现:defer 由 deferproc(登记)和 deferreturn(执行)两大函数支撑,基于 goroutine 私有的 _defer 链表实现;
  1. 参数规则:参数在登记时拷贝到 _defer 节点,执行时不再读取最新值;
  1. 执行顺序:新 defer 插入链表头,执行时后进先出,panic 时也会遍历执行;
  1. 性能优化:优先栈分配 _defer 节点,简单场景启用 openDefer 内联优化。

关键记忆点:defer 的 "登记 - 执行" 是链表 + 栈 / 堆分配的组合,参数拷贝是 "快照",执行是 "倒序",panic 时也能兜底执行。

相关推荐
Cache技术分享2 小时前
359. Java IO API - 路径比较与处理
前端·后端
Java水解2 小时前
SQL 核心概念:JOIN 和 UNION 到底有什么区别?
后端·sql
夜空下的星2 小时前
springboot实现Minio大文件分片下载
java·spring boot·后端
lizhongxuan2 小时前
Claude Mem:为什么长上下文不等于好记忆
后端
y = xⁿ2 小时前
重生之我创作出了小红书:对象存储模块,用户资料模块
后端·mysql·intellij-idea
404避难所2 小时前
windows安装WSL2
后端
轩情吖2 小时前
MySQL之用户管理
数据库·c++·后端·mysql·权限管理·用户管理
添尹3 小时前
Go语言基础之基本数据类型
开发语言·后端·golang
fffcccc11123 小时前
关于解决Eino不兼容音音频输入的问题
后端