错误需在第一现场判断并包装上报,关键路径用%w保留错误链,slog结构化日志+自定义Handler限流上报,按Temporary接口区分瞬时/永久错误,异步上报须用带cancel的context或缓冲chan控制goroutine生命周期。

错误发生时如何立刻触发警报而不是静默丢弃
Go 的 error 类型本身不带传播能力,直接 return err 后若上层没处理,警报就断了。必须在错误产生的第一现场做判断,而非依赖下游兜底。
- 对关键路径(如支付回调、数据库写入、第三方 API 调用)使用带上下文的错误包装:
fmt.Errorf("failed to sync order %s: %w", orderID, err),保留原始错误链 - 在包装后立即判断是否需告警:比如
errors.Is(err, context.DeadlineExceeded)或匹配特定错误码(如 HTTP 503、PostgreSQL unique_violation) - 避免在 defer 中发警报——defer 执行时机不可控,可能 panic 已发生或 goroutine 已退出
用 log/slog + hook 实现错误自动上报到 Prometheus Alertmanager
原生 slog 不支持钩子,但可通过自定义 slog.Handler 在日志级别为 LevelError 且含 "err" 字段时触发 HTTP 上报。
- 构造结构化日志时强制带
err键:slog.Error("db insert failed", "err", err, "table", "orders") - Handler 内检查
record.Level() == slog.LevelError且attrs中存在err值,再提取其类型、消息、栈(用fmt.Sprintf("%+v", err)获取) - 上报前加限流:用
time.AfterFunc延迟 100ms 发送,100ms 内同 error message 只发一次,防刷爆 Alertmanager
如何区分 transient error 和 permanent error 并设置不同告警策略
不是所有错误都该立刻告警。网络超时可重试,主键冲突大概率是业务逻辑问题——混在一起会淹没真实风险。
- 定义错误分类接口:
type Temporary interface{ Temporary() bool },让自定义错误实现它;标准库的net.OpError、os.SyscallError已实现 - 对
Temporary() == true的错误,只记录 + 打点(如metrics.Counter("error_transient_total", "op", "redis_get")),不触发 PagerDuty / 钉钉 - 对永久性错误(如
sql.ErrNoRows本不该出现、JSON 解析失败但输入确定合法),才走完整告警链路,并附上traceID和上游请求 ID
goroutine 泄漏导致错误无法上报怎么办
在 http handler 或定时任务中启 goroutine 处理错误上报,但没控制生命周期,会导致 goroutine 积压,后续错误根本发不出。
立即学习“go语言免费学习笔记(深入)”;
- 永远用带 cancel 的 context 启动上报 goroutine:
go func(ctx context.Context) { ... }(ctx),并在父函数退出前调用cancel() - 拒绝使用
go alert.Send(...)这种裸启动;改用封装好的异步队列,如基于chan error的有限缓冲通道(cap=100),配合后台 worker 拉取 - 在程序退出前调用
runtime.SetFinalizer检查是否有未完成的上报 goroutine(仅调试用,生产禁用)
真正难的不是“怎么发告警”,而是决定“哪条错误值得发”以及“发完之后有没有人看”。错误字段是否结构化、traceID 是否贯穿全链路、告警摘要里有没有可操作线索(比如 “user_id=U123456 缺少 email 校验” 而非 “validate failed”),这些细节比选什么告警渠道重要得多。










