go服务可观测性需从main()开始设计:日志须带trace id和上下文,用zap+context打通请求生命周期;健康检查要聚合依赖组件状态;指标采集需异步且鉴权;http中间件必须捕获goroutine级panic并统一错误处理。

为什么你的 Go 服务一出问题就只能看日志猜?
因为默认的 log 包不带 trace ID、不打上下文、不区分 level 语义,更不会自动关联 HTTP 请求或 RPC 调用链。可观测性不是加个 Prometheus 就完事,而是从第一行 main() 开始就得设计好数据怎么生成、怎么携带、怎么落地。
用 zap + context 打通请求生命周期
不要用 log.Printf,它无法继承 context,也无法注入 trace ID。必须让每条日志都携带当前请求的 context.Context,并从中提取 trace_id 和 span_id(哪怕你暂时没接入 OpenTelemetry)。
- 初始化 logger 时启用
zap.AddCaller()和zap.WithCaller(true),方便快速定位日志来源 - HTTP handler 中用
req.Context()构造带 trace ID 的子 context:ctx = context.WithValue(req.Context(), "trace_id", tid),再传给业务逻辑 - 所有日志调用必须走
logger.Info("xxx", zap.String("trace_id", tid)),别依赖全局 logger 自动注入 —— 它不会自动读 context - 如果用了
opentelemetry-go,优先用otel.GetTextMapPropagator().Extract()解析 header 中的 trace context,而不是手拼trace_id
暴露健康检查和指标端点时,别只返回 200
/healthz 返回 200 不代表数据库连得上、缓存没超时、下游服务没熔断。硬编码的健康检查等于没检查。
- 把每个依赖组件(DB、Redis、gRPC client)封装成
checker接口,实现Check(ctx) error方法 -
/healthz端点聚合所有 checker,任意一个失败就返回 503 + JSON 错误详情,例如:{"db": "dial timeout", "redis": "OK"} - 指标暴露用
promhttp.Handler()即可,但注意:避免在 handler 里做耗时操作(如实时查 DB),指标采集应走异步更新的prometheus.GaugeVec或CounterVec - 别把
/metrics暴露在公网;用 reverse proxy 做路径级鉴权,或限制监听地址为127.0.0.1:9090
HTTP 中间件里埋点要避开 panic 捕获盲区
Go 的 defer + recover 只能捕获当前 goroutine 的 panic,而 HTTP server 启动后,每个请求都在独立 goroutine 运行。如果你只在 main() 里 recover,根本抓不到 handler 里的 panic。
- 中间件必须包裹 handler 函数体,用
defer func() { if r := recover(); r != nil { logger.Error("panic recovered", zap.Any("panic", r)) } }() - 记录 panic 时,务必带上
runtime.Stack()的前 2KB,否则只有 panic message,没有堆栈,无法定位 - 别在中间件里直接
http.Error(w, ...),而应统一返回error类型,由最外层统一转成 HTTP 状态码和 body —— 这样才能确保错误被 metrics 统计、被 tracing 标记为 error - 注意
net/http默认会吞掉 handler panic 并打印到 stderr,这和你的日志系统割裂,必须显式拦截
真正难的不是加多少库,而是每一条日志、每一个 metric label、每一次 context 传递,都要想清楚它最终会在 Grafana 里怎么被筛选,在 Jaeger 里怎么被串联,在告警规则里怎么被触发。漏掉任意一环,可观测性就断在那儿了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











