错误日志中间件必须同时包装responsewriter并defer recover(),否则无法记录真实状态码或捕获panic;需重写writeheader/write、用zap.error()而非err.error()、区分error与panic日志、置于中间件链最外层且内聚recover逻辑。

错误日志中间件必须包装 ResponseWriter 并 recover panic
不包装 ResponseWriter 就记不到真实状态码,不加 defer recover() 就捕获不到 handler 内 panic,这两点漏掉一个,错误日志就形同虚设。
常见错误现象:http: multiple response.WriteHeader calls(重复写 header)、日志里全是 200 却实际返回了 500、panic 后进程直接退出没记录。
- recover 必须放在最外层 wrapper 的
ServeHTTP开头,且defer要紧贴函数起始 - 包装的
ResponseWriter必须重写WriteHeader和Write,否则 status 始终为 0 - panic 捕获后要立即调用
http.Error或自定义错误响应,不能只打日志就返回 - 别在 recover 里再调
next.ServeHTTP—— 请求已中断,再调会双写响应
用 zap.Error() 记录 error 而不是 err.Error()
标准库 log.Printf("%v", err) 只输出字符串,丢失堆栈、因果链和类型信息;zap.Error(err) 才能自动展开 Unwrap() 链、提取 StackTrace(),且字段名固定为 error,方便 ES/Kibana 过滤。
使用场景:数据库超时、HTTP client 返回非 2xx、JSON 解析失败等业务错误传播路径中的任意一环。
- 传入原始
error值,不要先.Error()再塞进zap.String() - 搭配
github.com/pkg/errors或 Go 1.13+ 的%w包装,才能保证链式展开 - 若用
slog,对应的是slog.Any("err", err),不是slog.String("err", err.Error())
区分 error 日志与 panic 日志的触发时机
handler 主动返回 err != nil(如校验失败、业务拒绝)属于 error 日志;未被处理的 panic 属于崩溃日志。两者语义不同,上报策略也该分开:前者可采样、可降级,后者必须 100% 记录并告警。
性能影响:panic 恢复本身开销极小,但频繁 panic 说明代码有缺陷,不应靠日志兜底;而主动 error 日志若带完整 body 或 stack,高并发下易拖慢吞吐。
- error 日志建议只记关键字段:
method、path、status、err、duration - panic 日志必须强制附加
stacktrace和goroutine id(可用runtime.Stack) - 敏感字段如
Authorization、password在 error 日志中也要脱敏,别以为“不是 panic 就不用管”
注册顺序决定能否捕获所有错误
错误日志中间件必须处于链路最外层,否则内层中间件 panic 会绕过它;同时要确保它在 recover 中间件之后(或合并在同一个 wrapper 里),否则 recover 不生效。
典型错误配置:http.Handle("/", logging(auth(handler))) —— logging 在最外层是对的,但如果 logging 里没 recover,auth 或 handler 的 panic 就漏掉了。
- 正确做法是把 recover 和错误日志写进同一个中间件函数,避免拆成两个独立 wrapper
- 如果用了
gorilla/mux或chi,注册时仍要用http.Handle("/", middleware(handler)),别用HandleFunc - 别依赖框架自带的 “error handler”,比如 Gin 的
c.Error()只影响当前请求上下文,不触发全局 panic 捕获
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











