4. ErrorHandler 基础:异常分类、错误捕获模式、Panic 与 Recover 机制
在可观测性体系中,ErrorHandler 是连接 Trace 监控与报警联动的关键枢纽。本章将深入剖析 Go 语言中的异常分类、错误捕获模式以及 Panic/Recover 机制,为后续构建健壮的报警联动系统打下坚实基础。
4.1 异常分类:从 Error 到 Panic
在 Go 语言中,异常并非单一概念,而是分为三个层次:预期错误 (Error)、非预期错误 (Panic) 和 致命错误 (Fatal)。理解它们的区别是设计 ErrorHandler 的第一步。
| 分类 | 触发场景 | 可恢复性 | Trace 联动方式 |
|---|---|---|---|
| Error | 业务逻辑校验、网络超时、文件不存在 | 是,通过返回 error 值 | 记录为 Span 的 Error Event |
| Panic | 数组越界、空指针、类型断言失败 | 是,通过 recover 捕获 | 记录为 Span 的异常状态 + Stack Trace |
| Fatal | 配置错误、端口被占用、数据库连接失败 | 否,进程退出 | 记录为 Span 的 Fatal 级别 + 报警联动 |
实践建议:在 Trace 监控中,Error 应作为 Span 的
status 属性标记,Panic 应作为 exception 事件记录,而 Fatal 则直接触发报警联动,无需等待 Trace 完成。
4.2 错误捕获模式:分层防御
一个成熟的 ErrorHandler 需要构建多层防御体系,确保任何层次的异常都能被正确捕获并关联到 Trace 上下文。
三层捕获模式:
- 业务层捕获:在 HTTP Handler 或 gRPC Interceptor 中统一捕获 error 返回值,并记录到当前 Span。
- 中间件层捕获:在 Gin/Echo 等框架的 Recovery 中间件中捕获 Panic,生成完整的 Stack Trace 并关联 Trace ID。
- 全局兜底捕获:在
main函数或 goroutine 入口处设置全局 Recover,防止未捕获的 Panic 导致进程崩溃。
// 业务层捕获示例:HTTP Handler 中的 Error 处理
func UserHandler(c *gin.Context) {
span, ctx := tracer.StartSpan(c.Request.Context(), "UserHandler")
defer span.End()
userID := c.Param("id")
user, err := db.GetUser(ctx, userID)
if err != nil {
// 记录 Error 到 Span
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
}
// 中间件层捕获示例:Gin Recovery 中间件
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
// 从 Context 中提取 Trace ID
traceID := c.GetString("trace_id")
// 记录 Panic 到日志系统
log.Errorf("Panic recovered: %v, trace_id: %s", r, traceID)
// 返回 500 并包含 Trace ID
c.AbortWithStatusJSON(500, gin.H{
"error": "internal server error",
"trace_id": traceID,
})
}
}()
c.Next()
}
}
4.3 Panic 与 Recover 机制:深度解析
Panic 是 Go 语言中一种特殊的控制流机制,它会导致当前 goroutine 的 deferred 函数链被逆序执行,直到遇到 recover 或程序崩溃。理解其执行流程对于设计可靠的 ErrorHandler 至关重要。
4.4 实战:ErrorHandler 与 Trace 联动
在 Trace 监控中,ErrorHandler 的核心职责是将异常信息转化为可观测的 Span 属性。以下是一个完整的联动示例:
// ErrorHandler 与 Trace 联动实现
type ErrorHandler struct {
tracer trace.Tracer
logger *zap.Logger
}
func (eh *ErrorHandler) HandleError(ctx context.Context, err error, msg string) {
span := trace.SpanFromContext(ctx)
if span != nil {
// 1. 记录错误事件
span.AddEvent(msg, trace.WithAttributes(
attribute.String("error.type", fmt.Sprintf("%T", err)),
attribute.String("error.message", err.Error()),
))
// 2. 设置 Span 状态为 Error
span.SetStatus(codes.Error, msg)
// 3. 记录 Stack Trace(可选)
span.SetAttributes(
attribute.String("error.stack", string(debug.Stack())),
)
}
// 4. 日志记录
eh.logger.Error(msg,
zap.Error(err),
zap.String("trace_id", trace.SpanFromContext(ctx).SpanContext().TraceID().String()),
)
}
// 使用示例
func ProcessOrder(ctx context.Context, orderID string) error {
_, span := eh.tracer.Start(ctx, "ProcessOrder")
defer span.End()
order, err := db.GetOrder(orderID)
if err != nil {
eh.HandleError(ctx, err, "failed to get order")
return err
}
// 正常处理...
return nil
}
避坑指南:
- 不要在
defer中直接调用recover()而不记录日志,否则 Panic 会被静默吞掉,导致 Trace 中丢失关键异常信息。 - 对于 goroutine 中的 Panic,必须在 goroutine 内部使用
defer recover()捕获,否则会导致整个进程崩溃。 - ErrorHandler 应始终接收
context.Context参数,以便提取 Trace ID 并关联到日志和报警系统。
4.5 异常分类与报警等级映射
为了与报警联动系统对齐,我们需要将异常分类映射到不同的报警等级:
| 异常分类 | 报警等级 | 联动动作 | 示例场景 |
|---|---|---|---|
| Error | Warning | 记录到 Trace + 日志 | 用户请求参数校验失败 |
| Panic (Recovered) | Critical | 记录到 Trace + 日志 + 发送报警 | 空指针异常被中间件捕获 |
| Panic (Unrecovered) | Emergency | 进程重启 + 立即报警 | goroutine 中未捕获的 Panic |
| Fatal | Emergency | 进程退出 + 报警联动 | 数据库连接失败导致服务不可用 |
核心原则:ErrorHandler 的设计目标是“不丢失任何异常,不错报任何正常”。通过将异常分类与 Trace 的 Span 状态、报警等级进行精确映射,我们可以实现从异常发生到报警触发的全链路可观测。