panic是运行时致命故障的紧急中止机制,用于程序已无法继续正确运行的场景;error则是可预期、可处理的业务失败状态,二者层级不同,不可混用。

panic不是错误处理,而是程序崩溃信号
Go里panic和error根本不在一个层级:前者是运行时致命故障的紧急中止机制,后者是业务逻辑中可预期、可检查、可传播的失败状态。你不能用panic替代return err——就像不能用“拉闸断电”来代替“关灯”。一旦panic发生且未被recover捕获,整个goroutine就完了,连日志都可能来不及刷。
-
error用于“这事没做成,但我知道为什么,还能补救”,比如文件不存在、网络超时、参数校验失败 -
panic只该用于“这事已不可能继续,再跑下去结果不可信”,比如配置加载失败导致数据库连接池根本建不起来、全局单例在init里被重复初始化、底层库被传入明显违反文档的nil指针 - 把“用户ID不存在”这种业务提示
panic("user not found"),等于把404当成系统宕机处理——调用方无法区分是bug还是提示,也无法返回HTTP 404,只能硬吞或崩溃
recover必须配合defer,且仅在defer函数内有效
想拦住panic,recover不是随便放哪儿都行。它只在defer注册的函数里调用才起作用,而且defer语句必须在panic发生前执行过——常见错误是把它写在if分支里,结果panic触发时defer根本没注册。
- 正确姿势:在函数开头就写
defer func() { if r := recover(); r != nil { /* 处理 */ } }() - 错误姿势:
if someCondition { panic("boom") }; defer func() { recover() }()—— 此时recover永远收不到东西 -
recover()返回interface{},通常转成fmt.Errorf("panic: %v", r)再往外传,保持接口统一;别直接返回r,类型不一致会破坏调用方错误处理逻辑 - 注意:recover后程序能继续执行,但panic发生时的现场(比如变量状态、锁持有情况)不会自动回滚——它不修复问题,只避免崩掉
哪些场景真该用panic?别替业务逻辑背锅
官方推荐的panic使用范围极窄,绝大多数时候你该写的不是panic,而是return fmt.Errorf(...)。只有当程序已处于“继续运行会产生错误结果”的状态时,才考虑panic。
- 服务启动阶段的关键失败:如
flag.Parse()后发现必要配置缺失、sql.Open()返回err != nil且无降级路径 - 绝对不该发生的逻辑分支:比如
switch typ { case TypeA, TypeB: ... default: panic("unknown type: " + typ),而这个typ本应是枚举常量,default理论上永不进入 - 底层库防御性检查:如
sync.Pool.Put(nil)直接panic,防止上层误用引发静默数据损坏 - 反例:HTTP handler里查数据库返回空结果、JSON解析字段缺失、用户输入格式不对——这些全该走
error返回+状态码控制
HTTP handler里recover的典型写法与陷阱
这是panic/recover最常被用到的真实场景:防止某个handler里的bug拖垮整个服务。但很多人只加recover,却忘了日志、状态码、上下文清理这些关键动作。
立即学习“go语言免费学习笔记(深入)”;
- 必须在handler最外层
defer里recover,且要立即记录日志:log.Printf("Panic in %s: %+v", r.URL.Path, p),否则panic堆栈丢失 - 响应必须是500,且绝不暴露panic详情给客户端:
http.Error(w, "Internal Server Error", http.StatusInternalServerError) - 别在recover里尝试“重试”或“兜底返回默认值”——panic说明状态已不可信,此时任何“假装成功”的返回都是危险的
- 如果handler里用了
context.WithTimeout或database/sql连接,recover后记得手动关闭资源(如rows.Close()),因为panic会跳过正常return路径
最常被忽略的一点:recover只是让程序不死,但它不解决panic的根源。上线后看到recover日志,第一反应不应该是“幸亏拦住了”,而是立刻查代码——那个panic本不该发生,它是个bug信号,得修,而不是包一层recover就完事。










