fiber 中不能用 context.context 透传 trace id,因其 *fiber.ctx 不继承原生 context 且不自动传播;应使用 c.locals 安全传递,并在跨协程时显式传参,手动设置 traceparent 头。

Fiber 框架里不能直接用 context.Context 透传 Trace ID,因为 Fiber 的 *fiber.Ctx 是独立封装的,不继承 Go 原生 context.Context,强行塞进去会导致跨协程丢失或 panic。
为什么 Fiber 的 context.Context 不可用
Fiber 的 *fiber.Ctx 内部确实持有原生 context.Context(可通过 c.Context() 获取),但它在每次请求生命周期中是只读、不可替换的;你调用 context.WithValue() 返回的新 context 不会被 Fiber 自动传播到中间件、子协程或下游 HTTP 客户端。更关键的是:Fiber 默认不把该 context 绑定到 goroutine 本地存储,一旦发起并发子协程(比如异步上报、DB 查询),context.Value() 就失效。
常见错误现象:
- 在中间件里用
ctx := context.WithValue(c.Context(), traceKey, traceID),然后在 handler 里取不到 - 用
go func() { log.Printf("%v", ctx.Value(traceKey)) }()打印出nil - 调用
fasthttp.Client.Do(req)后,下游服务收不到traceparent头
正确做法:用 c.Locals + 中间件统一注入
Fiber 提供了线程安全、协程隔离的本地存储机制 c.Locals,它底层基于 sync.Map 并绑定到当前 *fiber.Ctx 实例,天然支持在同一个请求链路内跨中间件、handler 传递数据,且不会被子协程意外污染。
实操建议:
- 在入口中间件(如
TraceIDMiddleware)中生成或提取trace-id,存入c.Locals("trace_id") - 所有需要埋点的地方(DB、Redis、HTTP 调用)都从
c.Locals("trace_id")读,不要碰c.Context() - 若需发起子协程并携带 trace,必须显式传参,例如:
go reportSpan(c.Locals("trace_id").(string), ...) - 对下游 HTTP 请求,手动设置
traceparent头:req.Header.Set("traceparent", formatTraceParent(...))
示例代码片段:
func TraceIDMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
traceID := c.Get("traceparent")
if traceID == "" {
traceID = uuid.New().String()
}
c.Locals("trace_id", traceID)
return c.Next()
}
}
跨协程调用时 Trace ID 显式透传的关键点
Fiber 不做协程上下文自动继承,所以任何 go func() 或第三方异步库(如 gofiber/fiber/v2/middleware/cors 内部异步逻辑)都不会自动拿到 c.Locals。这是最常被忽略的断裂点。
必须做到:
- 所有异步任务启动前,从
c.Locals提取 trace 相关字段,作为参数传入闭包或函数 - 避免在子协程里调用
c.Locals—— 此时c已超出作用域,可能 panic 或返回空值 - 使用
fasthttp发起 HTTP 请求时,别依赖req.SetRequestContext()(它不生效),老老实实手动设 header - 如果用了 OpenTelemetry SDK,确保初始化时禁用自动 context 注入:
otelhttp.NewTransport(http.DefaultTransport, otelhttp.WithPropagators(propagator)),再配合手动 header 设置
容易踩的坑:Locals 类型断言失败与并发安全
c.Locals 是 map[string]interface{},存的时候没类型检查,取的时候容易 panic。尤其当多个中间件写同一个 key(比如都写 "trace_id"),但类型不一致(string vs uuid.UUID),后续断言就会崩。
规避方式:
- 约定 key 命名空间,如
"trace.id"、"trace.span"、"trace.parent" - 取值时加类型判断:
if id, ok := c.Locals("trace.id").(string); ok { ... } - 不要在中间件里修改已存在的 trace 字段,只读取、只透传;生成和格式化统一由入口中间件完成
- 切勿把
*fiber.Ctx整个传进子协程 —— 它不是线程安全的,且生命周期无法保证
真正麻烦的从来不是怎么加一个 header,而是 trace ID 在 fasthttp 请求、goroutine、channel、回调函数之间跳来跳去时,哪一环悄悄丢了又查不出原因。盯住 c.Locals 的读写位置和子协程的参数传递路径,比堆一堆 SDK 更管用。











