panic只能在defer中捕获且仅限当前goroutine,需为每个goroutine单独布防;HTTP服务应通过中间件统一recover并记录堆栈,按类型和路径聚合统计。

panic发生时如何捕获并记录堆栈
Go的recover只能在defer函数中生效,且仅对当前goroutine有效。这意味着你不能在任意位置“全局拦截”panic——必须提前在可能出错的goroutine入口处布防。
常见错误是把recover写在主函数外层却期望它能捕获子goroutine的panic,结果日志里完全看不到异常信息。
实操建议:
- 每个独立启动的goroutine(尤其是
go func() { ... }())都应包裹自己的defer/recover逻辑 -
recover()返回interface{},需用fmt.Sprintf("%v", r)转成可读字符串,直接fmt.Println(r)可能输出<nil> - 用
debug.Stack()获取完整堆栈,比runtime.Caller更可靠——后者只到recover调用点,而前者能定位到panic源头
如何区分业务panic和系统panic
不是所有panic都该被统计为错误:比如panic("unimplemented")可能是开发期占位符,panic(io.EOF)可能是设计上的终止信号。硬性全量统计会污染监控指标。
立即学习“go语言免费学习笔记(深入)”;
推荐做法是约定panic值类型或前缀:
- 业务代码统一用自定义错误类型panic,例如
panic(&AppError{Code: "DB_TIMEOUT", Msg: "xxx"}) - 或统一用字符串前缀,如
panic("ERR_DB_CONN_FAILED: ..."),后续按"ERR_"过滤 - 避免
panic(nil)或panic(0)这类无意义值,它们无法分类,也难调试
注意:recover()拿到的值可能是*runtime.Error(如index out of range),这类属于真正的运行时崩溃,必须告警,不应归入业务统计维度。
在HTTP服务中统一处理panic
HTTP handler是panic高发区,但标准http.ServeMux不提供panic钩子。直接在每个handler里写defer/recover太重复。
实操方案是封装中间件:
func PanicRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC in %s %s: %v\n%v", r.Method, r.URL.Path, r, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 使用
http.Handle("/api/", PanicRecovery(http.StripPrefix("/api", apiHandler)))
关键点:
- 必须在
next.ServeHTTP调用前注册defer,顺序反了就捕获不到 - 不要在recover后继续调用
next.ServeHTTP——panic已破坏当前执行流,继续跑可能引发二次panic - 若需返回结构化错误(如JSON),需检查
w.Header().Get("Content-Type")再决定响应格式,避免text/plain和application/json混用
统计维度与落地存储建议
单纯打印日志不够,需要可聚合、可告警的统计。重点不是“发生了多少次panic”,而是“哪类panic在哪个服务/路径/时间段集中爆发”。
推荐字段至少包含:
-
panic_type:取fmt.Sprintf("%T", recovered),区分*errors.errorString、*AppError等 -
stack_hash:对debug.Stack()做sha256哈希,用于去重和聚类相同堆栈 -
path(HTTP场景)或goroutine_id(后台任务)作为上下文标识 - 时间戳用
time.Now().UnixMilli(),方便时序分析
存储上,短期可用本地文件+logrotate,长期建议接入Prometheus(暴露panic_total{type="DB_TIMEOUT",path="/api/user"}这类指标)或ELK。别用内存map计数——进程重启就丢,且goroutine并发写map会panic。
最易被忽略的一点:recover后未重置goroutine状态(比如未关闭channel、未释放锁),可能让后续逻辑静默失败。panic统计只是起点,真正要解决的是为什么这里会panic。


















