Go服务中请求ID追踪需确保context.Context全程透传:从HTTP头或生成ID注入context,goroutine需显式传参,日志用结构化方式注入,跨服务调用须手动透传header,任一环节遗漏将导致链路中断。

Go 服务里加请求 ID 追踪,不是加个中间件就完事——关键在于 context.Context 要贯穿全程,否则 ID 在 goroutine 分叉或异步调用后就丢了。
怎么把 RequestID 注入 context 并透传
HTTP 请求进来时,从 header(比如 X-Request-ID)或自动生成一个唯一 ID,塞进 context.WithValue。但别直接用裸字符串当 key,得定义类型避免冲突:
type ctxKey string const requestIDKey ctxKey = "request_id"
然后在中间件里:
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
- 必须用
r.WithContext()生成新 *http.Request,原r的 context 不可变 - 下游所有函数都要显式接收
context.Context参数,不能只靠闭包捕获 - 日志库(如
log/slog或zap)需配置 context hook 或手动注入,否则 log 里看不到 ID
goroutine 启动后 RequestID 为什么突然没了
Go 的 go func() {}() 不会自动继承父 goroutine 的 context,这是最常踩的坑。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
go doSomething()——doSomething拿不到当前 request context - 正确写法:
go doSomething(ctx),且doSomething函数签名必须是func(ctx context.Context) - 如果调用链里有第三方库(比如数据库驱动、HTTP client),确认它是否接受 context;
db.QueryContext、http.Client.Do都支持,但老版本sqlx默认不传,得手动 wrap
log 输出里怎么稳定带上 RequestID
不用改每条 log.Printf,优先走结构化日志 + context 提取。
- 用
slog.With("req_id", ctx.Value(requestIDKey))包一层 logger,后续所有slog.Info都带上了 - 更省事:写个
slog.Handler实现,在Handle方法里自动从 context 提取 ID 并注入attrs - 注意:slog 默认不传播 context,所以 handler 必须在每个 log 调用时重新查
ctx.Value,不能在初始化时缓存 - 如果用了
zap,别依赖zap.AddCaller(),它不处理 context,要用zap.String("req_id", getReqID(ctx))显式加
跨服务 HTTP 调用时 RequestID 怎么透传
出向请求必须手动把当前 RequestID 写进 header,不然链路就断了。
- 构造 client request 前,从 context 取 ID:
id := ctx.Value(requestIDKey).(string) - 设置 header:
req.Header.Set("X-Request-ID", id) - 别漏掉重试逻辑——每次重试都该复用同一个 ID,而不是新生成一个
- 如果调用 gRPC,对应 header 是
metadata.MD{"x-request-id": []string{id}},不是 HTTP header
真正难的不是加 ID,而是确保它不被任何一层中间件、异步任务、子协程、日志输出或跨协议调用吃掉。只要有一处没接住,整条链路就变成盲区。


















