必须显式透传 context.Context 中的 trace_id,所有边界点(goroutine、HTTP、DB等)都不能用 context.Background();错误包装需注入 trace_id 并保留错误链;日志须每次动态注入 trace_id 字段;接口设计需强制 ctx 为第一参数。

怎么让 trace_id 真正透传到每个日志和错误里
靠全局变量或中间件自动注入 context 是行不通的——goroutine 启动、HTTP client 调用、DB 查询这些边界点,context.Context 不会自动继承。必须在每个跳转点显式传入带 trace_id 的 ctx,否则日志和错误里就只剩空字段。
常见错误是 handler 里解析了 X-Request-ID,但调用 dal.GetUser(ctx, id) 时传的是 context.Background();或者启动 goroutine 时写 go fn() 而不是 go fn(ctx)。
- 入口处生成或提取
trace_id:优先从traceparent(W3C 标准)头读取, fallback 到X-Request-ID或uuid.NewString() - 用自定义 key 注入:
type ctxKey string; const traceIDKey ctxKey = "trace_id",避免字符串 key 冲突 - 所有下游调用必须透传该
ctx:DB、HTTP client、消息队列、子 goroutine —— 不能中途换为context.Background() - 异步任务(如
go func() { ... }())必须把ctx当参数传进去,否则新协程完全脱离链路
错误包装时怎么带上 trace_id 和调用位置
单纯用 fmt.Errorf("xxx: %w", err) 只加业务上下文,不带 trace_id 和堆栈;而 errors.Wrap(err, "msg")(来自 github.com/pkg/errors)能捕获堆栈,但默认不包含 trace 上下文。
真正有用的做法是:在错误产生处(比如 DB 层),用 fmt.Errorf 包装时,同时注入 trace_id 字段,并确保堆栈可展开。
立即学习“go语言免费学习笔记(深入)”;
- 推荐组合:
fmt.Errorf("db query failed (trace_id=%s): %w", getTraceID(ctx), err)—— 这样日志里一眼可见 trace,且%w保留原始错误链 - 若需完整堆栈,改用
github.com/pkg/errors的errors.Wrapf(err, "db query failed (trace_id=%s)", getTraceID(ctx)),再用fmt.Printf("%+v", err)查看 - 切忌在 handler 层反复包装:比如
dal已包装过一次,service层又fmt.Errorf("get user failed: %w", err)—— 堆栈重复、trace_id 多份、日志冗余 - 对外返回错误前要脱敏:
err.Error()或自定义 message,别把%+v结果直接暴露给客户端
怎么让 zap/logrus 自动打上当前 trace_id
zap.Logger 和 logrus.Entry 本身不感知 context.Context,所谓“自动”其实是每次日志调用前手动提取 + 注入字段。不存在初始化一次 logger 就一劳永逸的事。
最稳妥的方式是封装一个上下文感知的日志函数,而不是依赖全局 logger 的固定字段。
- zap 推荐:
logger.With(zap.String("trace_id", getTraceID(ctx))).Info("user created")—— 每次调用都动态取值,不污染实例 - logrus 推荐:
log.WithField("trace_id", getTraceID(ctx)).Info("user created")—— 注意不要用log.WithField(...).Info(...)链式后复用该 entry,它会缓存字段值 - 别在服务启动时一次性设置 logger 的 global fields:那只会固化成启动时刻的
trace_id,后续请求全错 - 如果用 OpenTelemetry,可配合
otel.GetTextMapPropagator().Extract()解析traceparent,再从 span 中取SpanContext().TraceID().String()
跨模块暴露 context-aware 接口的实操要点
模块间传递 context.Context 不是“加个参数”就完事——接口设计决定了 trace 是否能贯穿到底。暴露的函数签名必须显式接收 ctx,且内部所有下游调用都必须透传它。
例如 dal.GetUser(ctx, id) 如果签名是 GetUser(id int),那上层无论怎么传 ctx 都没用;同理,http.Client.Do(req) 若没用 req.WithContext(ctx),调用链就断在 HTTP 边界。
- 所有导出函数第一参数必须是
ctx context.Context,哪怕暂时没用也要预留 —— 这是未来支持追踪的契约 - DB 层封装时,用
sqlx.GetContext(ctx, ...)替代sqlx.Get(...);HTTP client 用req.WithContext(ctx) - 避免模块内自己 new context:
context.WithTimeout(context.Background(), ...)是典型反模式,应传入 parent ctx 后派生 - 错误返回值类型保持标准
error,不要返回自定义 error struct —— 否则上层无法用errors.Is或%w继续包装


















