Go defer 中再次 panic 时怎么保留原始 panic 信息
如果业务函数已经 panic,defer 里的清理动作又 panic,最容易丢掉的是“最初为什么失败”。稳妥的做法不是依赖运行时打印出的两段信息,而是在外层 deferred function 里直接 recover 原始值,再把清理动作放进一个局部 recover 边界;两者同时失败时,重新 panic 一个同时保存两类值的结构。
先捕获原始 panic,再捕获清理 panic,最后按“只有原始、只有清理、两者都有”三种情况决定重新抛出什么。这样调用方拿到的值是稳定可识别的,而不是被清理代码偶然覆盖。
recover必须直接调用于 deferred function,抽成普通 helper 会失效。- 清理逻辑要放进局部闭包,让它自己的 panic 不破坏原始 panic 的变量。
- 同时失败时用带字段的
PanicChain保存原始值和清理值,调用方再决定记录或转成 error。
为什么清理阶段的第二次 panic 会让原始信息变得不可靠
Go 的 panic 会沿当前 goroutine 的调用栈展开,相关 defer 按后进先出执行。正常情况下,defer 只负责关闭资源、解锁或写入收尾信息;但如果清理代码本身访问了失效对象、重复关闭了不安全资源,或者主动调用了 panic,就形成了第二个 panic。
这时不要把运行时最终输出当作业务协议。程序日志可能同时出现两条 panic,但调用方真正能通过 recover 消费的是当前 panic 值。更可靠的做法是自己定义组合值,把“业务异常”和“清理异常”放在不同字段里。
| 场景 | 应该保留的值 | 处理建议 |
|---|---|---|
| 业务正常,清理正常 | 无 | 正常返回 |
| 业务 panic,清理正常 | 原始 panic | 原样重新 panic |
| 业务正常,清理 panic | 清理 panic | 抛出清理值 |
| 业务和清理都 panic | 两者 | 抛出 PanicChain |

先在外层 recover 原始 panic,再隔离清理 panic
关键细节是 recover 的位置。它只有在 panic 展开期间、并且由 deferred function 直接调用时,才能拿到当前 panic 值。因此不能写成 defer recoverOriginal(),也不能在普通函数里间接调用它。
package panicchain
import "fmt"
// PanicChain 同时保存业务阶段和清理阶段的 panic 值。
type PanicChain struct {
Original any // 业务函数最先抛出的值
Cleanup any // 清理函数后来抛出的值
}
// Error 让 PanicChain 也能被日志和 error 处理代码识别。
func (p PanicChain) Error() string {
return fmt.Sprintf("original panic: %v; cleanup panic: %v", p.Original, p.Cleanup)
}
// capturePanic 只负责隔离一次清理调用,不让它继续向外覆盖原始值。
func capturePanic(cleanup func()) (value any, panicked bool) {
defer func() {
if r := recover(); r != nil {
value, panicked = r, true // 在本层记录清理 panic
}
}()
cleanup()
return nil, false
}
// Run 在一个 goroutine 内执行 work,并把清理异常纳入同一条 panic 信息。
func Run(work, cleanup func()) {
defer func() {
original := recover() // 必须由这个 deferred function 直接调用
cleanupValue, cleanupPanicked := capturePanic(cleanup)
switch {
case original != nil && cleanupPanicked:
panic(PanicChain{Original: original, Cleanup: cleanupValue})
case original != nil:
panic(original) // 只有业务失败时保留原值
case cleanupPanicked:
panic(cleanupValue) // 只有清理失败时抛出清理值
}
}()
work()
}
这里的 capturePanic 使用自己的 deferred function 直接调用 recover,所以清理 panic 会被转换成普通返回值。外层 defer 仍然掌握最终决定权,cleanup 不会被重复调用。

根据三种结果决定是否重新 panic
这段代码的分支顺序很重要。业务 panic 存在时,cleanup 必须先执行,但不能让 cleanup 的异常直接冲出当前 defer;只有在两个值都拿到后,才构造 PanicChain。业务正常而 cleanup 失败时,则没有原始值可保留,直接抛出清理值即可。
调用方可以在自己的边界统一记录:
defer func() {
if r := recover(); r != nil {
if chain, ok := r.(PanicChain); ok {
// 两类异常分别记录,避免只看最后一条 panic 文本。
log.Printf("work panic=%v cleanup panic=%v", chain.Original, chain.Cleanup)
return
}
log.Printf("panic=%v", r) // 兼容普通 panic 值
}
}()
如果系统不允许 panic 穿过边界,也可以在这里把 PanicChain 转换为显式 error。不要在 Run 内悄悄吞掉异常,否则调用方会误以为清理和业务都成功。
生产代码里还要守住哪些边界
- 能返回 error 的清理函数优先返回 error;只有必须保持 panic 语义的边界,才使用上面的组合值。
- 多个清理动作最好先合并成一个 cleanup 函数,明确执行顺序,避免多个独立 defer 互相覆盖语义。
- 每个 goroutine 都要在自己的入口设置 recover;一个 goroutine 的 defer 无法替另一个 goroutine 接住 panic。
- 测试至少覆盖四个表格分支,并断言
PanicChain.Original和PanicChain.Cleanup的动态类型没有丢失。
还有一个实际取舍:如果清理失败只影响诊断,不影响资源安全,可以记录后继续抛原始 panic;如果清理失败意味着数据可能未落盘,就应该把两者组合后交给上层,而不是为了“保持原始值”把清理问题隐藏掉。
相关问题
为什么不能把 recover 抽成普通函数?
因为 Go 规范要求它由 deferred function 直接调用。普通 helper 中的 recover 不处在这个直接调用位置,通常只能得到 nil。
panic 值一定是 error 吗?
不一定。它可以是字符串、结构体、指针或其他接口值,所以 PanicChain 用 any 保存,不能强制断言成 error。
另一个 goroutine 的 panic 能在这里恢复吗?
不能。panic 和 recover 只在同一个 goroutine 的展开链上生效;启动 goroutine 时应在 goroutine 入口单独设置 defer。
检测机构出具报告前怎么核对样品编号和原始记录
- 上一篇
- 检测机构出具报告前怎么核对样品编号和原始记录
- 下一篇
- 青铜月桂叶手机壁纸怎么做出复古但不带徽标
-
- Golang · Go问答 | 30分钟前 |
- Go defer 修改命名返回值时为什么需要用指针或闭包
- 198浏览 收藏
-
- Golang · Go问答 | 43分钟前 | go · 迭代器 · 函数类型 · range over func iter.Seq Go迭代器
- Go range over function 迭代器如何把停止信号传给生产方
- 210浏览 收藏
-
- Golang · Go问答 | 55分钟前 |
- Go t.Helper 标记辅助函数后失败行号会怎样变化
- 283浏览 收藏
-
- Golang · Go问答 | 57分钟前 | 测试 · 模糊测试 · Go问答 · 回归测试 · testdata/fuzz testing.F seed corpus Go fuzz f.Add
- Go fuzz 测试的 seed corpus 怎么保留可复现输入
- 389浏览 收藏
-
- Golang · Go问答 | 58分钟前 |
- Go testing.T.Deadline 怎么让子测试提前停止昂贵准备
- 107浏览 收藏
-
- Golang · Go问答 | 58分钟前 | 接口 · go · 泛型 · interface generics Comparable
- Go 泛型类型参数的 comparable 约束为什么不能覆盖所有接口值
- 454浏览 收藏
-
- Golang · Go问答 | 58分钟前 |
- Go pprof 标签怎么让同一服务的 CPU 数据按租户拆分
- 404浏览 收藏
-
- Golang · Go问答 | 59分钟前 | goroutine · go · runtime/trace · trace.NewTask ·
- Go trace.NewTask 如何标记一个跨 goroutine 的业务任务
- 208浏览 收藏
-
- Golang · Go问答 | 59分钟前 |
- Go runtime/metrics 如何读取调度器指标而不依赖 pprof
- 214浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 61次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 217次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 145次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 79次使用
-
- Generrated
- Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
- 56次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- golang中的defer函数理解
- 2022-12-22 366浏览
-
- 一文搞懂Go语言中defer关键字的使用
- 2022-12-30 485浏览
-
- GolangDefer基础操作详解
- 2022-12-22 444浏览
-
- 详解golang中的闭包与defer
- 2023-01-07 461浏览

