责任链中每个 Handler 必须显式接收并传递 context.Context:方法签名含 ctx 参数,调用下游时原样或派生后传入;仅存 traceID、userID 等只读元数据于 context;Handler 结构体不缓存 ctx;HTTP 入口须从 r.Context() 获取初始 ctx;末尾 DB/HTTP 调用必须使用 QueryContext/DoContext。

责任链里每个 Handler 都要显式接收 ctx context.Context
责任链不是自动透传 Context 的魔法结构——它只是函数串调用。如果你写 h1.Handle(req) 而没把 ctx 传进去,那下游就彻底丢失了取消信号和超时。常见错误是只在第一个 Handler 入口接收 ctx,后面全靠闭包捕获或 struct 字段缓存,结果一并发就错乱。
正确做法是:每个 Handler 方法签名必须带 ctx 参数,且调用下一个 Handler 时原样或派生后传入:
func (h *AuthHandler) Handle(ctx context.Context, req Request) Response-
next.Handle(ctx, req)—— 不加包装直接传 - 若需加超时(比如 DB 查询限 500ms),用
context.WithTimeout(ctx, 500*time.Millisecond)派生后再传,不覆盖原ctx
WithValue 只存 traceID、userID 这类元数据,别塞业务实体
责任链中常想“把用户对象从 AuthHandler 传给 OrderHandler”,于是用 context.WithValue(ctx, userKey{}, user)。这看似方便,但会立刻触发两个问题:下游无法从函数签名看出依赖,测试时还得手动构造完整 context;更严重的是,如果 OrderHandler 想更新用户邮箱,它改不了——context 是不可变的,只能返回新 context,而链上其他 Handler 还拿着旧的。
真正该放进去的只有请求级元数据:
- traceID(用于日志串联)
- userID(只读标识,非完整 User 结构)
- requestID、region、tenantID 等无状态标识符
- 绝不要放
*sql.DB、http.Client或可变结构体指针
别在 Handler struct 里存 ctx 字段
有人为图省事,在中间件 struct 里加一个 ctx context.Context 字段,初始化时塞进去,后续所有 Handle() 都直接用它。这是高危操作:这个 ctx 会被所有 goroutine 共享,一旦上游取消,它可能还在被其他请求复用;更糟的是,如果这个 struct 生命周期比单次请求长(比如全局注册的 middleware 实例),就会导致 goroutine 泄漏——子 context 持有已取消的父 context 引用,GC 清不掉 timer 和 channel。
必须坚持“每次请求新建链,每次调用传新 ctx”:
- Handler struct 里不存
ctx - 每个
Handle()方法都是无状态的,只依赖入参ctx和req - goroutine 启动前,确保用当前
ctx派生(如context.WithCancel(ctx)),并由启动它的 Handler 负责defer cancel()
HTTP handler 到责任链入口的衔接点最容易断掉 ctx
Go 的 http.Handler 接口不暴露 context.Context,但 http.Request 自带 .Context() 方法。很多人在 handler 函数里写 ctx := context.Background() 再传进责任链,等于主动切断了连接生命周期和客户端断连通知。
正确衔接方式只有一种:
- 从
r.Context()拿初始ctx,不是context.Background() - 若责任链需要额外控制(如统一加 3s 超时),用
context.WithTimeout(r.Context(), 3*time.Second)派生 - 绝不缓存
r.Context()到包级变量或 struct 字段——它绑定单次请求,复用即出错
最易忽略的是:责任链末尾的 DB 或 HTTP 调用,必须显式使用 QueryContext、DoContext 等带 context 的方法,否则前面所有传递都白做。

















