应自定义 recovery 中间件,在 defer 中 recover panic 并检查 c.writer.status()≥500,统一用带 level 字段的 apperror 区分错误等级,异步发送报警且避免阻塞请求。

中间件里怎么捕获 panic 和 HTTP 错误
Gin 默认的 recovery 中间件只打印 panic 日志,不提供修改行为的入口。要触发报警,必须自己写中间件,在 defer 里 recover 后做判断和上报。
常见错误类型包括:panic(如空指针、数组越界)、status >= 500 的响应、特定业务错误码(如 err.Code == "DB_TIMEOUT")。不能只依赖 status,因为有些 panic 发生在写响应头之后,status 已设为 200,但实际没返回数据。
- 用
gin.Context.AbortWithError()触发的错误不会进 recovery,需在中间件链中提前拦截 - 必须在自定义 recovery 中间件里调用
c.Error()记录错误,否则c.Errors为空 - 不要在 recover 后调用
c.JSON()或c.Status(),可能已写 header,会 panic
如何区分真实 panic 和业务主动抛出的 error
Gin 的 c.Error() 和 panic 都会存到 c.Errors,但来源不同:前者是显式调用,后者是 runtime 捕获。报警逻辑需要分开处理——比如 panic 必须告警,而 ErrValidation 只记录日志。
推荐做法是统一用自定义 error 类型,带字段标识严重等级:
type AppError struct {
Code string
Message string
Level string // "warn", "error", "critical"
}
func (e *AppError) Error() string { return e.Message }
在中间件里检查 c.Errors 最后一个 error 是否实现了 AppError 接口,再按 Level 决定是否发报警。
- panic 恢复后生成的 error 默认 Level 是
"critical" - 业务层调用
c.Error(&AppError{Level: "error"})才进报警通道 - 避免用字符串匹配 error message 判断类型,易出错且难维护
报警触发时机选 defer 还是 c.Next() 之后
必须选 defer —— 因为 panic 只能在 defer 里 recover;而 HTTP 错误(如 500)必须在 c.Next() 之后检查 c.Writer.Status(),否则 status 还是 0。
一个中间件里要同时处理两种情况,结构如下:
func AlertMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
status := c.Writer.Status()
if status >= 500 {
triggerAlert(c, fmt.Sprintf("HTTP %d", status))
}
defer func() {
if err := recover(); err != nil {
triggerAlert(c, fmt.Sprintf("PANIC: %v", err))
}
}()
}
}
注意:上面代码有 bug —— defer 在函数开头注册,但 panic 发生在 c.Next() 期间,所以没问题;但 triggerAlert 不能依赖 c.Request 在 panic 后是否有效(某些极端 panic 会让 request nil),应提前备份关键字段如 c.FullPath()、c.ClientIP()。
- panic 后
c.Request.URL可能 panic,务必用c.Request.URL.String()前加 nil 检查 - 报警内容里别打完整 stack trace,Gin 的
gin.DefaultWriter会截断,建议用debug.Stack()单独捕获 - 多个中间件共用同一套报警逻辑时,用
c.Set("alert_triggered", true)防重复触发
报警发送失败怎么办,要不要重试
报警本身失败(如网络超时、钉钉 webhook 403)不能阻塞主请求流程,必须异步且无感。同步调用报警接口会导致请求延迟飙升,尤其在高并发下雪崩。
最简方案是起 goroutine 发送,但需控制并发和失败兜底:
- 用带缓冲的 channel + worker pool 控制并发,缓冲区满则丢弃(报警可丢,请求不能卡)
- 失败时写本地文件(如
/tmp/alert-failures.log),由外部脚本定时扫并重发 - 别用
log.Printf替代报警——它只是日志,不是告警动作 - 测试时把 webhook 地址配成
http://127.0.0.1:12345,用netcat -l 12345验证是否发出
真正容易被忽略的是 context 超时对报警的影响:如果请求本身设置了 c.Request.Context().Done(),goroutine 里再用这个 context 发请求会立刻 cancel。报警必须用 background context 或带独立 timeout 的 context。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











