Go 中不能用 panic 做业务错误处理,因其会中断 goroutine 且需 defer+recover 拦截,仅适用于空指针、越界等不可恢复场景;业务错误应统一用自定义 error 类型(如 *AppError)显式返回,并通过中间件统一转换为 HTTP 响应。

Go 中为什么不能用 panic 做业务错误处理
因为 panic 会中断当前 goroutine 的执行流,且无法被常规 if err != nil 捕获——它必须靠 recover 配合 defer 才能拦截,而 recover 只在 defer 函数中有效,且仅对当前 goroutine 生效。业务错误(比如参数校验失败、数据库记录不存在)是预期内的分支逻辑,不是程序崩溃信号。
常见误用现象:http.HandlerFunc 里直接 panic("user not found"),结果服务返回 500 而非 404;或在中间件里 recover 后没重置 HTTP 状态码,导致错误被吞掉。
- 业务错误该用
error类型显式返回,由调用方决定如何响应(重试、降级、返回特定状态码) -
panic应仅用于真正不可恢复的场景:空指针解引用、数组越界、断言失败等 - HTTP handler 中统一 recover 是可行的,但必须手动设置
w.WriteHeader(500)并写入结构化错误体
如何封装 error 实现上下文透传和分类
Go 1.13 引入的 errors.Is 和 errors.As 让错误判断不再依赖字符串匹配,但前提是错误链里有可识别的底层类型。推荐用自定义错误类型 + 包装器组合实现。
例如定义一个带错误码、HTTP 状态码和 traceID 的基础错误:
立即学习“go语言免费学习笔记(深入)”;
type AppError struct {
Code string
Status int
Message string
Cause error
TraceID string
}
func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Cause }
这样就能用 errors.Is(err, ErrNotFound) 判断,也能用 errors.As(err, &e) 提取原始结构做日志或响应构造。
- 不要用
fmt.Errorf("failed to parse %s: %w", input, err)就完事——丢失了业务语义 - 所有外部依赖(DB、RPC、HTTP client)返回的错误,都应包装成
*AppError再向上抛,避免下游直接依赖第三方 error 类型 - 日志记录时用
fmt.Sprintf("%+v", err)可展开整个错误链,看到每一层的文件/行号
HTTP 中间件如何统一捕获并转换 error
核心思路是:handler 函数签名改为返回 error,中间件用闭包包装后统一处理。不要在每个 handler 里重复写 if err != nil { w.WriteHeader(...); json.NewEncoder(w).Encode(...) }。
典型模式:
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
func ErrorHandler(next HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := next(w, r); err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
w.WriteHeader(appErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": appErr.Message})
return
}
// 兜底:未知错误一律 500
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "internal error"})
}
})
}
- 注意:
w.WriteHeader()必须在任何Write之前调用,否则会被忽略 - 如果 handler 内部已调用
json.NewEncoder(w).Encode(...),再进中间件就晚了——必须确保所有业务逻辑不直接写 response body - 中间件里别用
recover()处理业务错误,那是补救措施,不是设计原则
什么时候该用 errors.Join,什么时候该自己实现 MultiError
errors.Join 适合临时聚合多个独立错误(比如并发调用多个下游,全部失败),但它返回的是 interface{},没法用 errors.As 提取具体类型,也不带额外字段(如状态码)。真实项目中更常需要的是可分类、可序列化的多错误容器。
例如批量操作失败时,要区分哪些成功、哪些因权限拒绝、哪些因资源冲突:
type MultiAppError struct {
Errors []*AppError
}
func (m *MultiAppError) Error() string {
return fmt.Sprintf("multiple errors: %d failed", len(m.Errors))
}
func (m *MultiAppError) As(target interface{}) bool {
if e, ok := target.(*AppError); ok {
for _, err := range m.Errors {
if errors.Is(err, e) || errors.As(err, target) {
return true
}
}
}
return false
}
-
errors.Join适合测试或工具函数内部快速拼接,不适合生产 API 响应 - 自定义
MultiError要实现As方法才能参与标准错误判断链 - HTTP 响应里返回多错误时,别只给个字符串 summary,要提供明细数组(含 code/status/message),前端才好针对性提示
*AppError,所有 fmt.Errorf 必须带 %w,所有 handler 必须走统一中间件——这些靠代码审查和 linter(比如 errcheck、自定义 golangci-lint 规则)来守住,而不是靠文档。









