必须在http handler最外层用defer+recover捕获panic,因panic不跨goroutine传播且中间件中recover无效;捕获后需设500状态码、记录含堆栈和traceid的告警、异步发送并限流。

panic捕获必须在HTTP handler最外层用defer+recover
Go的panic不会自动传播到上层中间件,一旦在handler内部触发,会直接终止当前goroutine并向上冒泡——如果没被recover住,整个HTTP请求就会500中断,且无日志可查。中间件里写defer recover()没用,因为中间件函数本身早执行完了。
正确做法是在最终handler调用前包一层兜底:用http.HandlerFunc包装原始handler,在其内部做defer recover()。常见错误是把recover()放在中间件函数体里,结果根本捕不到panic。
- 必须在
func(w http.ResponseWriter, r *http.Request)最内层加defer func() { if err := recover(); err != nil { ... } }() -
recover()只对当前goroutine有效,不能跨goroutine捕获(比如goroutine里启的异步任务panic了,这里捕不到) - 捕获后要手动设置
http.Error(w, "Internal Server Error", http.StatusInternalServerError),否则响应状态码可能是200但body为空
报警内容要包含panic堆栈、请求上下文和traceID
只打印err字符串(比如"runtime error: invalid memory address")毫无定位价值。真正有用的报警信息必须带三样东西:panic原始值、完整堆栈、当前请求的标识字段。
堆栈要用debug.Stack()获取,不是fmt.Sprintf("%+v", err);请求上下文至少包括r.Method、r.URL.Path、r.Header.Get("X-Request-ID")(如果用了trace中间件)。漏掉traceID,告警来了也找不到链路。
- 别用
log.Print(err),它不带堆栈;用log.Printf("panic: %+v\n%s", err, debug.Stack()) - 从
context.Context里取traceID比从Header取更可靠(如果中间件已注入ctx) - 敏感字段如
r.Header.Get("Authorization")必须脱敏,避免报警消息泄露密钥
告警通知走异步通道,别阻塞HTTP响应
发邮件、调企业微信机器人、推Prometheus Alertmanager——这些IO操作慢且可能失败。如果在recover()后同步执行,会导致HTTP响应卡住,超时或连接被复位,用户看到的是空白页或连接重置,而不是500。
必须把告警逻辑扔进独立goroutine,或者投递到缓冲channel由后台goroutine消费。同步调用http.Post是典型反模式。
- 推荐用带缓冲的
chan string(比如make(chan string, 100)),避免告警堆积导致内存暴涨 - goroutine里要做recover,防止告警发送逻辑自己panic拖垮整个服务
- 失败时记录本地日志(
log.Printf("alert send failed: %v", err)),别静默丢弃
生产环境务必限制告警频率,防告警风暴
一个bug引发连续panic,每秒打10次告警,运维手机会被炸飞。必须做速率限制,常见做法是按panic类型+请求路径做滑动窗口计数,比如“5分钟内同一panic消息最多告警3次”。
用sync.Map存key → lastAlertTime太粗糙,容易误杀;建议用golang.org/x/time/rate的Limiter,每个panic组合(fmt.Sprintf("%s|%s", panicType, path))配一个limiter实例。
- key里别只用panic字符串,要拼上
r.URL.Path,否则不同接口同个panic会互相干扰限流 - limiter的burst设为1,rate设为
3/300(5分钟3次),避免突发流量误触发 - 首次panic立刻告警,后续在窗口期内的相同panic只记日志不通知
panic本身不可预测,但告警怎么发、发多少、发给谁,这些控制点必须收口在统一模块里,散落在各handler里的recover逻辑迟早会失控。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











