Go无全局panic捕获,recover仅对同goroutine有效;需分层处理:HTTP中间件、goroutine启动点、CLI主函数各加defer/recover,并用%w包装错误实现链路追踪。
recover 只对当前 goroutine 有效很多人误以为 Go 能像 Node.js 或 Python 那样设置一个顶层错误处理器。实际上,recover 必须在 defer 中配合 panic 使用,且仅能捕获**同一 goroutine 内**发生的 panic。主 goroutine 崩溃、HTTP handler 中未捕获的 panic、或新起的 goroutine(如 go func(){}())里 panic,都无法被主流程的 recover 拦截。
这意味着:你不能靠一个“全局 defer + recover”兜住所有错误。必须分层设计:
defer/recover
go 关键字的地方,自己加 defer/recover
main 函数末尾加 defer/recover,仅覆盖主流程fmt.Errorf 的 %w 实现错误链路追踪Go 1.13 引入的错误包装(%w)是统一处理的基础。它让错误可嵌套、可判断、可展开,避免丢失原始上下文。
不要这样写:
return errors.New("database insert failed")
而应这样包装上层原因:return fmt.Errorf("failed to save user: %w", err)
这样后续可用 errors.Is(err, sql.ErrNoRows) 或 errors.As(err, &pgErr) 判断底层错误类型。
常见踩坑点:
%v 或 %s 替代 %w → 错误链断裂,无法用 errors.Is/As
err.Error() → 丢失堆栈和原始错误类型errors.Wrap(来自 github.com/pkg/errors)→ 与标准库不兼容,Go 1.20+ 已不推荐典型 Web 服务中,90% 的运行时错误发生在 handler 执行期间。统一中间件能避免每个 handler 重复写 defer/recover 和错误响应逻辑。
示例中间件结构:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("PANIC in %s
%s: %+v", r.Method, r.URL.Path, r)
}
}()
// 包装 ResponseWriter,捕获 5xx 状态码并记录
wr := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wr, r)
if wr.statusCode >= 500 {
log.Printf("HTTP %d from %s %s", wr.statusCode, r.Method, r.URL.Path)
}
})
}
注意要点:
http.Handle("/", ErrorHandler(r)))responseWriter 是自定义 wrapper,用于监听实际写出的状态码os.Exit(1) 显式终止,并确保日志落盘命令行工具(如 cli/cmd/root.go)或定时任务中,错误不应静默吞掉。统一出口能保证失败可观察、可告警。
推荐模式:
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("FATAL PANIC: %+v", r)
os.Exit(1)
}
}()
if err := run(); err != nil {
log.Printf("FATAL ERROR: %v", err)
os.Exit(1)
}
}
关键细节:
log.Printf 后立即 os.Exit(1),避免日志缓冲未刷出log.Fatal —— 它会直接调用 os.Exit(2),且不可拦截、不可测试zap),确认其 Sync() 被调用,否则可能丢日志%w)、panic 是否在正确 goroutine 被 recover、以及每一类入口(HTTP / CLI / goroutine)是否都有明确的错误出口。漏掉其中任何一层,都会导致错误静默或难以定位。