Go无全局panic捕获,recover仅对同goroutine有效;需分层处理:HTTP中间件、goroutine启动点、CLI主函数各加defer/recover,并用%w包装错误实现链路追踪。

Go 中没有全局 panic 捕获机制,recover 只对当前 goroutine 有效
很多人误以为 Go 能像 Node.js 或 Python 那样设置一个顶层错误处理器。实际上,recover 必须在 defer 中配合 panic 使用,且仅能捕获**同一 goroutine 内**发生的 panic。主 goroutine 崩溃、HTTP handler 中未捕获的 panic、或新起的 goroutine(如 go func(){}())里 panic,都无法被主流程的 recover 拦截。
这意味着:你不能靠一个“全局 defer + recover”兜住所有错误。必须分层设计:
- HTTP 服务层:每个 handler 包一层中间件做
defer/recover - goroutine 启动点:凡用
go关键字的地方,自己加defer/recover - 命令行 CLI:
main函数末尾加defer/recover,仅覆盖主流程
用自定义 error 类型 + 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) 判断底层错误类型。
常见踩坑点:
立即学习“go语言免费学习笔记(深入)”;
- 用
%v或%s替代%w→ 错误链断裂,无法用errors.Is/As - 在日志中只打印
err.Error()→ 丢失堆栈和原始错误类型 - 用
errors.Wrap(来自 github.com/pkg/errors)→ 与标准库不兼容,Go 1.20+ 已不推荐
HTTP handler 统一错误中间件:拦截 panic 和返回 error
典型 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,用于监听实际写出的状态码 - 不要在中间件里尝试 “重写” error 为 success —— 错误就该是错误,掩盖只会让调试变难
CLI 或后台任务中的错误出口:用 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)是否都有明确的错误出口。漏掉其中任何一层,都会导致错误静默或难以定位。










