2026-11-08-Go-defer-panic-recover面试

你好,我是利威尔xu。

这是「面试八股」专栏里 Go 错误处理方向的一篇。之前聊了并发十连问、内存管理十连问,这期聊 Go 里最容易被低估的三个语法特性------defer、panic、recover。它们看起来简单,但坑极多,面试官最爱拿它们设陷阱。

建议先收藏,文末那张"defer/panic/recover 坑位表",面试前扫一眼能救你一命。

文章目录

    • 为什么这三个语法糖要单开一篇
    • [第 1 问:defer 的参数是什么时候求值的?](#第 1 问:defer 的参数是什么时候求值的?)
    • [第 2 问:多个 defer 的执行顺序是什么?](#第 2 问:多个 defer 的执行顺序是什么?)
    • [第 3 问:defer 和 return 的先后顺序,返回值会被改吗?](#第 3 问:defer 和 return 的先后顺序,返回值会被改吗?)
    • [第 4 问:panic 之后,defer 还会执行吗?](#第 4 问:panic 之后,defer 还会执行吗?)
    • [第 5 问:recover 为什么有时候"救不回来"?](#第 5 问:recover 为什么有时候"救不回来"?)
    • [第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?](#第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?)
    • [defer/panic/recover 坑位表(建议收藏)](#defer/panic/recover 坑位表(建议收藏))
    • 结尾

为什么这三个语法糖要单开一篇

defer、panic、recover 是 Go 错误处理的三件套。表面看:defer 延迟执行、panic 抛异常、recover 捕获异常,三句话讲完。但一旦问到"defer 的参数什么时候求值""defer 的执行顺序""recover 为什么有时救不回来",大多数人就露馅了。

这三件套的坑,是 Go 面试里"看起来都会、答起来全错"的重灾区。

第 1 问:defer 的参数是什么时候求值的?

答案框架 :defer 的参数在声明 defer 那一刻就求值了,而不是在执行 defer 时求值。

go 复制代码
func f() {
    i := 1
    defer fmt.Println(i)  // 参数 i 在这里就求值,是 1
    i = 100               // 后面改 i,不影响 defer 里的 1
}
// 输出:1

但如果 defer 的是一个闭包,闭包里捕获的是变量引用,执行时才读最新值:

go 复制代码
func f() {
    i := 1
    defer func() { fmt.Println(i) }()  // 闭包捕获引用
    i = 100
}
// 输出:100

追问应对:"为什么参数求值在声明时?"------因为 defer 本质是"把函数调用和它的实参先存起来,等函数返回时再执行"。存起来的时候,实参已经算好了。

第 2 问:多个 defer 的执行顺序是什么?

答案框架 :后进先出(LIFO),像叠盘子一样,最后 defer 的最先执行。

go 复制代码
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 输出:3 2 1

追问应对:"这个顺序有什么用?"------典型用法是资源成对释放:先打开资源 A,再打开资源 B,defer 关闭时 B 先关、A 后关,顺序正好反着来,符合"先开的后关"的资源管理逻辑。

第 3 问:defer 和 return 的先后顺序,返回值会被改吗?

答案框架 :这是最经典的陷阱。defer 在 return 之后、函数真正返回之前 执行,所以命名返回值可以被 defer 修改。

go 复制代码
func f() (result int) {
    defer func() { result++ }()  // 命名返回值,defer 能改
    return 5
}
// 返回 6,不是 5

因为 return 5 其实是"先把 5 赋给命名返回值 result,再执行 defer,最后真正返回"。defer 里的 result++ 把 result 从 5 改成了 6。

追问应对:"如果返回值不是命名的呢?"------匿名返回值,defer 改不了。因为匿名返回值在 defer 执行时,值已经拷到返回位置了,defer 里改的是另一个副本。这就是"命名返回值才能被 defer 改"的原因。

第 4 问:panic 之后,defer 还会执行吗?

答案框架 :会 。panic 会沿着调用栈向上传播,每一层函数的 defer 都会被执行(这正是 defer 用来做清理的原因),直到被 recover 捕获或程序崩溃。

go 复制代码
func f() {
    defer fmt.Println("清理资源")  // panic 后仍会执行
    panic("出错了")
}
// 输出:清理资源,然后 panic

追问应对:"panic 后 defer 执行有什么用?"------保证资源释放、日志落盘。比如打开了文件/连接,panic 时 defer 里的 close 仍会执行,不会泄漏。这是 defer 的核心价值。

第 5 问:recover 为什么有时候"救不回来"?

答案框架 :recover 只有在 defer 函数里直接调用 才有效,且只能捕获当前 goroutine 的 panic。

无效的 recover:

go 复制代码
func f() {
    recover()              // 直接调用,无效,panic 继续传播
    panic("boom")
}

有效的 recover:

go 复制代码
func f() {
    defer func() {
        if r := recover(); r != nil {  // 必须在 defer 里
            fmt.Println("捕获到:", r)
        }
    }()
    panic("boom")
}

两个关键限制:

  1. recover 必须在 defer 函数里直接调用(不能嵌套在别的函数里)。
  2. recover 只能捕获本 goroutine 的 panic,别的 goroutine 的 panic 救不了。

追问应对 :"子 goroutine panic 了主 goroutine 能 recover 吗?"------不能。每个 goroutine 的 panic 只能自己 recover。所以生产上,子 goroutine 里的 panic 要么自己 recover,要么用 errgroup/统一 defer recover 兜底,否则整个进程崩溃。

第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?

答案框架:这是一道考工程素养的题。

用 error(绝大多数情况):可预期的、可恢复的错误,比如文件不存在、网络超时、参数非法。这些是"正常业务逻辑的一部分",用 error 返回让调用方处理。

用 panic(少数情况):不可恢复的、程序性错误,比如"slice 越界""空指针解引用""断言失败"。这些是"代码写错了",不该让调用方"优雅处理"。

一句话 :能预见的错误用 error,程序性的 bug 用 panic。 把 panic 当成正常的错误处理手段(比如用 panic/recover 做流程控制)是反模式。

追问应对 :"Go 标准库是怎么做的?"------标准库几乎都用 error,只有真正的程序错误(如 index out of range)才 panic。这体现了 Go 的哲学:错误是值,显式处理,而不是异常。

defer/panic/recover 坑位表(建议收藏)

考点 结论 常见错误答案
defer 参数求值 声明时求值 说"执行时求值"
defer 闭包 捕获引用,执行时读最新值 混淆参数和闭包
defer 顺序 后进先出 LIFO 说"先进先出"
defer 改返回值 命名返回值可被改 说"defer 改不了返回值"
panic 后 defer 会执行 说"panic 后 defer 不执行"
recover 位置 必须在 defer 里直接调 说"recover 随便调都行"
recover 范围 只能本 goroutine 说"能跨 goroutine 捕获"
panic vs error 可预期用 error,程序 bug 用 panic 说"一律用 panic"

结尾

defer、panic、recover 这三个语法糖,坑就藏在"求值时机""执行顺序""作用范围"这些细节里。吃透这张坑位表,面试里的相关陷阱题基本就踩不着了。

你被问过最刁钻的一道 defer 题是什么?是不是"defer 改返回值"或"recover 救不回来"这种?

下一篇面试八股,我接着聊 Go 的 interface 和反射,还是这个专栏。别走丢。

作者:利威尔xu | CSDN 专栏《面试八股》

相关推荐
CoderYanger6 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
wo_jack17 小时前
C++ 入门笔记(十六):第16课 vector 模拟实现十问十答 —— 从三指针到迭代器失效
c++·后端·面试·stl·vector
聪明的笨猪猪20 小时前
AI应用架构选型:架构师的核心决策困境与权衡
经验分享·面试·架构
JWASX21 小时前
【Go】map 源码解析 - group
后端·golang·map·源码分析
疯华整猫1 天前
被杀掉之后:我把 Android 的「唤起 + 常驻」重做了一遍
前端·面试·架构
nice_lcj5201 天前
Java 并发容器入门:ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列
java·面试·并发编程·阻塞队列·concurrenthashmap·copyonwritearraylist
岁月宁静1 天前
AI 应用开发工程师简历怎么写:HR 初筛、技术复筛、面试追问,三道关卡各看什么?
人工智能·python·面试
Dream it possible!2 天前
LeetCode 面试经典 150_多维动态规划_交错字符串(97_C++_中等)
算法·leetcode·面试·动态规划
yeccheng2 天前
MySQL 索引超详解:从底层 B+ 树到面试高频考点
java·开发语言·jvm·mysql·面试