recover必须在发生panic的同一函数内通过defer调用才有效,仅能捕获当前函数内的panic;需在defer匿名函数中直接调用recover并配合debug.Stack()记录上下文,不可提取为独立函数或用于捕获第三方库panic。

recover必须在defer中调用,且仅对当前函数panic有效
解析器内部出错时,panic往往发生在深层嵌套的递归调用里(比如词法分析、AST构建),但recover只能捕获**同一函数内**发生的panic。这意味着你不能在顶层Parse函数外另写一个recover去“兜底”——它必须被包在Parse自己的defer里,否则返回nil。
常见错误是把recover写在独立辅助函数中:
func parseExpr() {
panic("unexpected token")
}
func Parse(input string) (s *Syntax, err error) {
defer safeRecover(&err) // ❌ safeRecover里调用recover(),但此时已不在panic发生栈帧
parseExpr()
}
正确做法是让defer直接包裹匿名函数,并在其中调用recover:
- 必须用
defer func() { ... }(),不能提前提取成命名函数 - 命名返回值
err需在defer中赋值,才能透出到调用方 - 不要依赖外部变量捕获状态,因为
panic后原函数后续逻辑不会执行
捕获后应附加堆栈和原始panic值,而非静默吞掉
解析器崩溃时,用户需要知道“在哪一行、哪个token、为什么失败”,而不仅仅是"internal error"。Go标准库runtime/debug.Stack()能拿到完整调用链,但要注意:它只在panic传播过程中有效,一旦recover执行完毕,堆栈信息就不可靠了。
立即学习“go语言免费学习笔记(深入)”;
推荐组合方式:
- 用
recover()获取原始panic值(可能是string、error或自定义类型) - 立即调用
debug.Stack(),并在defer闭包内完成日志或错误构造 - 避免在
recover后继续调用可能再次panic的函数(如未加锁的map写入)
示例:
func Parse(input string) (s *Syntax, err error) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
err = fmt.Errorf("parse internal panic: %v\n%s", r, stack)
}
}()
// 实际解析逻辑...
}
不应对其他包的panic做无差别恢复
如果你的解析器调用了第三方库(比如正则引擎、JSON解析器),它们内部的panic不应由你来recover。原因很实际:
- 你无法控制其内部状态是否一致(比如缓冲区是否损坏、句柄是否泄漏)
- 它们本该用
error返回可预期错误,panic说明已越界——此时强行恢复只会让后续解析更错乱 - net/http等标准库的recover是特例,因为它明确设计为“隔离请求级错误”,你的解析器没有这个契约
安全边界很清晰:只恢复你自己代码路径中主动panic("unclosed brace")这类可控异常;对json.Unmarshal等调用,应检查其error返回,而不是包一层defer/recover。
recover后无法回滚已发生的副作用
解析器常维护状态:已分配的AST节点、已推进的lexer位置、已缓存的符号表条目。一旦panic发生,这些中间状态大概率已污染。而recover只是让函数return,**不会撤销任何已执行的语句**。
这意味着:
- 不要指望
recover后还能“重试”或“修正”语法树——应该直接放弃当前解析分支 - 若使用全局/共享状态(如
var currentScope *Scope),panic前必须确保其一致性,或改用函数局部状态 - 更健壮的做法是:把解析逻辑拆成纯函数式小单元,输入输出明确,失败即丢弃整个子结果,而非依赖recover清理
真正容易被忽略的一点:很多解析器在recover后仍试图复用部分AST节点,结果把半截错误结构塞进最终结果里——这比崩溃更难调试。


















