必须通过 http.Request.Context() 注入请求 ID 并全局复用 snowflake Node,避免 UUID 丢失、ID 冲突及 context 断链;日志需桥接 context 值并缓存,goroutine 必须显式传参 ctx。

并发请求下直接用 uuid.New() 会丢 ID
不是 UUID 本身不唯一,而是它和请求生命周期完全脱钩。你在 handler 里调用 uuid.New(),这个值只在当前 goroutine 里有效;一旦进 middleware、打日志、调下游服务、启新 goroutine(比如异步发消息),ID 就断了。你得手动把 ID 塞进每个函数参数、每个 context、每个结构体字段——漏一处,链路就断。
常见错误现象:log.Printf("req %s started", uuid.New().String()) → 每次日志都生成新 ID,上下游对不上;或中间件里取不到 ID,只能打空字符串或 panic。
- 必须通过
http.Request.Context()注入,且由统一中间件完成 - 不要用字符串做
context.Key,定义私有类型如type requestIDKey struct{} - 下游取值时必须做类型断言 + 非 nil 检查:
if id, ok := ctx.Value(requestIDKey{}).(string); ok { ... }
snowflake 节点 ID 配置错会导致 ID 冲突
用 github.com/bwmarrin/snowflake 时,snowflake.NewNode(1) 的参数是 worker ID,不是随便填的整数。它必须在集群内全局唯一,且不能超出 1023(10 位限制)。如果两个服务实例都传 1,又恰好在同一毫秒生成 ID,就会撞。
使用场景:K8s 环境下推荐从环境变量或 Downward API 注入,比如 os.Getenv("HOSTNAME") 哈希后模 1024;Docker Compose 可用 container_number 映射;本地开发建议固定为 0,但上线前必须改。
立即学习“go语言免费学习笔记(深入)”;
- 起始时间戳(Epoch)要统一,否则不同服务生成的 ID 时间部分可能回退,触发重试逻辑
- 别在每次请求里 new 一个 Node 实例,它是线程安全的,应全局复用
- 如果机器时间被 NTP 回拨,
node.Generate()默认 panic,生产环境需捕获并降级(比如 fallback 到 UUID)
跨 goroutine 时 context.Context 不会自动传递
Go 的 context.Context 只在父子 goroutine 间显式传递。你写 go doSomething(req.Context()) 是对的;但写 go func() { /* 这里 req.Context() 已失效 */ }() 就完蛋了——闭包捕获的是旧引用,或者更糟,是 nil。
典型坑:HTTP handler 里启动 goroutine 处理耗时任务(如写 DB、发通知),没把 context 传进去,导致超时控制失效、ID 丢失、取消信号收不到。
- 永远显式传参:
go processAsync(ctx, data),而不是依赖闭包 - 避免在 goroutine 里调
req.Context()——req可能已返回,req.Context()返回的是已 cancel 的 context - 如果必须用闭包,确保捕获的是传入的
ctx变量,不是req对象
日志库不读 context.Value,得自己桥接
标准库 log 和多数轻量日志封装(比如 logrus 默认配置)根本不看 context。你在中间件里塞了 ID,日志函数还是打不出它。
最简可行方案:封装一层 Logger,构造时接收 context.Context,内部缓存 ID 值(不是每次都 ctx.Value()),再透传给底层日志器。
- 高频日志下,反复调
ctx.Value()有性能损耗,缓存一次即可 - 别把
context.Context存成全局变量或长期持有,它只应在请求生命周期内有效 - 如果用
zerolog或zap,它们支持WithContext()方法,可直接集成,但注意别让 logger 持有 context 超出请求范围
实际最难的不是选算法,是让 ID 在 goroutine 分叉、中间件嵌套、异步回调、日志输出这些环节里始终不丢、不错、不乱。每个环节都得主动拉一把,而不是指望“框架自动处理”。


















