go无日志拦截器,recover仅能捕获同goroutine的panic并需在defer中调用,日志重定向指控制输出目标而非拦截内容;业务error应显式返回而非依赖recover。

Go 里没有“日志拦截器”这种东西——log 包本身不提供拦截能力,所谓“捕获并重定向底层异常”,本质是两件事:在 panic 发生时用 recover 拦住它,并把错误信息交由你选的日志系统(如 zap、slog)结构化输出;而“重定向”指的是控制日志写入目标(文件、网络、LTS 等),不是拦截日志内容本身。
为什么不能直接包装 log.Logger 实现拦截
log.Logger 是一个简单封装,其 Output 方法只负责写字符串到 io.Writer,不暴露错误类型、上下文或调用栈来源。你无法从 log.Printf("err: %v", err) 这行代码里反推出:这是 HTTP handler 报的错?来自哪个 goroutine?有没有 requestID?有没有嵌套 error?强行在 io.Writer 层做“解析日志字符串→识别异常→重定向”既不可靠又破坏结构化日志原则。
常见错误现象:
- 用
os.Pipe()接住log.SetOutput(),再起 goroutine 读取并转发——导致日志延迟、goroutine 泄漏、panic 堆栈被截断 - 在自定义
io.Writer.Write()里正则匹配"panic"或"recover"字样——误判率高,且无法区分业务 error 和真正 panic - 把
log.Fatal当作“重定向入口”,结果整个进程退出,而不是仅终止当前请求
真正有效的 panic 捕获点只在顶层 handler defer 中
Go 的 recover 必须在 defer 中调用,且只能捕获**当前 goroutine** 的 panic。HTTP server 启动后,每个请求都在独立 goroutine 中执行,所以捕获逻辑必须落在每个请求生命周期的最外层。
实操建议:
- 不要在任意工具函数里加
defer/recover,只在 HTTP handler 函数体第一行或中间件最外层放 - 确保
recover()后调用http.Error(w, ..., 500)或等效响应,否则客户端收不到状态码 - 用
debug.Stack()获取完整堆栈,别只打r(它只是 panic 参数,不含调用链) - 若用
slog,应传入slog.Group("panic", slog.String("stack", string(debug.Stack()))),而非拼接字符串
示例片段:
func MyHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
slog.Error("panic caught",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("stack", string(stack)),
slog.Any("panic_value", r),
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// ... 业务逻辑
}
业务 error 不该进 recover,而要靠统一返回签名拦截
90% 的“异常”其实是 error 值:参数校验失败、DB 查询为空、第三方 API 返回 401。这些根本不会触发 panic,recover 完全无效。强行用它兜底,只会掩盖设计缺陷——比如 handler 明明该返回 400 Bad Request,却被当成 500 处理。
正确路径是让 handler 显式返回 error:
- 定义类型:
type HandlerFunc func(http.ResponseWriter, *http.Request) error - 中间件检查返回值:
if err := h(w, r); err != nil { renderError(w, r, err) } -
renderError内部用errors.As(err, &e)断言*AppError,提取e.Code设状态码,而非硬编码 500 - 所有业务错误走
fmt.Errorf("xxx: %w", underlying),保持错误链可追溯
这样,日志记录点就明确落在 renderError 里,你可以在这里统一注入 traceID、method、path,并决定是否上报监控——这才是可控的“重定向”。
跨 goroutine panic 怎么办?别指望 recover
比如你在 handler 里启了个 goroutine 做异步通知,它 panic 了:recover 在主 goroutine 里完全看不见。Go 不允许跨 goroutine 捕获 panic,这是语言设计使然。
能做的只有三件事:
- 在新 goroutine 内部自己加
defer/recover,并把错误发到 channel 或通过回调通知主线程 - 用
context.WithCancel控制生命周期,避免 goroutine 孤儿化 - 对关键后台任务(如定时同步),用
sync.Once+ 全局 error 变量做降级兜底,但注意并发安全
试图用全局 recover 钩子捕获所有 goroutine panic,是 Go 新手最常踩的坑——它根本不存在。
复杂点在于:panic 和 error 的处理边界必须划清。recover 只管真正的崩溃(空指针、切片越界、map 写入 nil),其余一律走 error 返回;而日志系统只该接收结构化数据,不该承担“解析文本→识别异常”的职责。这两条线一旦混在一起,后续排查成本会指数上升。











