绝大多数微服务场景下 panic 不该 recover,尤其 HTTP/gRPC 中主动 recover 是反模式;仅插件加载等沙箱场景需 recover 并记录堆栈返回 500;启动期 panic 应 os.Exit(1)。

Go 微服务中 panic 该不该 recover
绝大多数微服务场景下,panic 不该被 recover,尤其在 HTTP handler 或 gRPC server 方法里主动 recover 是反模式。它掩盖了本该暴露的编程错误(如 nil pointer dereference、slice bounds),让故障难以复现和定位。
真正需要 recover 的只有极少数边界:比如插件系统加载第三方代码、模板渲染等明确允许“沙箱失败”的场景。即便如此,也要记录完整堆栈并返回明确错误码(如 500 Internal Error),而非静默吞掉。
- HTTP handler 中
defer func() { if r := recover(); r != nil { log.Error(...); http.Error(...) } }()→ 隐藏 bug,禁止 - gRPC server 的
UnaryInterceptor中全局 recover → 导致超时、重试逻辑失效,应只 wrap 已知可恢复错误 - 数据库连接池初始化失败、配置解析失败等启动期 panic → 应提前校验,失败直接
os.Exit(1)
error 类型该不该包装,怎么包装才不丢上下文
必须包装,但要用标准方式:fmt.Errorf("failed to fetch user %d: %w", userID, err)。用 %w 而非 %v 或 + "...",才能保留原始 error 的类型信息和链式调用能力(如 errors.Is()、errors.As())。
常见错误是层层拼接字符串导致错误溯源断裂:
立即学习“go语言免费学习笔记(深入)”;
err = fmt.Errorf("service A: failed to call B: %v", err) // ❌ 丢失原始类型
err = errors.Wrap(err, "service A failed") // ❌ 非标准,且需额外依赖
正确做法:
- 下游调用后立即包装,带上关键参数(ID、URL、method)
- 避免在中间层重复包装同一错误(如 service → repo → db 层各包一次)
- 日志中用
log.Error("call downstream", "err", err),zap/zapcore 会自动展开 error chain
gRPC 和 HTTP 错误码如何对齐业务语义
gRPC 的 codes.Code 和 HTTP 状态码不能硬映射。比如 gRPC codes.NotFound 对应 HTTP 404 没问题,但 codes.InvalidArgument 在 HTTP 侧可能对应 400(参数格式错)或 422(语义校验失败),得按业务判断。
推荐做法是定义统一错误码枚举,再由网关层翻译:
type ErrorCode int
const (
ErrUserNotFound ErrorCode = iota + 1000
ErrInvalidEmail
ErrPaymentDeclined
)
// HTTP handler
if errors.Is(err, ErrUserNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
}
// gRPC server
if errors.Is(err, ErrUserNotFound) {
return status.Error(codes.NotFound, "user not found")
}
- 不要依赖 gRPC status code 做业务分支(如
if code == codes.PermissionDenied { ... })→ 应用层应通过 error 类型或自定义字段判断 - HTTP 响应体中需带 machine-readable code 字段(如
{"code": 1001, "message": "email invalid"}),方便前端统一处理 - 所有错误响应必须带 trace ID,否则跨服务排查无从下手
分布式链路中错误传播的陷阱
Go context 本身不携带 error,所以 context.Context 不能用来传错;但你很容易在 span 中漏掉 error 标记,导致链路追踪里看到一个“成功”调用,实际内部已失败。
关键点:
- OpenTracing / OpenTelemetry 的 span 必须显式调用
span.SetStatus(...)和span.RecordError(err),否则错误不会上报到 Jaeger / Grafana Tempo - 不要在 defer 中 record error —— 如果函数返回
nilerror,但 defer 里又 record 了上层 err,会造成误报 - 跨服务调用时,原始 error 的 message 不要直接透传(含敏感信息/堆栈),而应提取结构化字段(如
errCode,retryable)塞进 response header 或 payload
最常被忽略的是:当服务 A 调用 B 失败后 fallback 到本地缓存,此时链路里 A → B 是失败 span,但 A 自身返回成功——这要求你在 A 的 span 上手动标记 “fallback taken”,否则监控里看不出降级行为。










