不能直接用 context.background() 创建子 context,因为会切断父子链路导致取消信号无法透传,引发资源泄漏;正确做法是用父 context 派生(withcancel/timeout/deadline/value),确保取消传播和生命周期可控。

为什么不能直接用 context.Background() 创建子 Context
因为这样会切断父子链路,上游取消信号无法透传到下游。比如 HTTP handler 中启动 goroutine 做异步任务,若子 Context 没继承 request 的 ctx,那客户端断连或超时后,goroutine 仍会继续跑完——资源泄漏风险就在这儿。
正确做法永远是用父 Context 派生:context.WithCancel、context.WithTimeout、context.WithDeadline 或 context.WithValue。它们返回的子 Context 内部保留了父指针,取消传播靠的就是这个链表结构。
context.WithCancel 和 context.WithTimeout 怎么选
看是否需要主动控制生命周期:WithCancel 适合手动触发终止(如收到 stop signal),WithTimeout 更适合有明确耗时上限的场景(如调用下游接口)。
注意:WithTimeout(parent, d) 底层其实是 WithDeadline(parent, time.Now().Add(d)),所以时间精度依赖系统时钟;如果父 Context 已过期,子 Context 会立即进入 Done 状态,不会等自己的 timeout 到期。
- 不要对同一个父 Context 多次调用
WithTimeout创建多个子 Context 并并发等待——它们共享取消通道,一个超时会同时关闭全部 - 若需独立超时控制(比如并行发三个请求,各自 5s 超时),必须分别用原始父 Context 派生
-
WithTimeout返回的cancel函数建议显式调用(哪怕没超时),避免 goroutine 泄漏:它会清理内部 timer 和 channel
什么时候该用 context.WithValue,又为什么多数时候不该用
WithValue 只适合传**请求范围的、不可变的元数据**,比如用户 ID、trace ID、request ID。它不是用来传业务参数的“快捷方式”。
常见误用:把 struct、handler、DB 连接塞进 Context——这破坏了函数签名可读性,也容易引发内存泄漏(value 引用大对象且生命周期被 Context 拖长)。
- Key 类型强烈建议用私有 unexported 类型,避免第三方包 key 冲突:
type ctxKey string; const userCtxKey ctxKey = "user" - 取值时务必判空:
v := ctx.Value(userCtxKey); if v != nil { ... },否则 panic - Go 1.21+ 支持
context.WithValue的 key 类型检查(编译期提示),但 runtime 仍不做类型校验,运行时类型断言失败会 panic
父子 Context 传递中容易被忽略的坑
最隐蔽的问题是:在 goroutine 中使用闭包捕获外部变量而非显式传入 Context,导致实际用的是创建 goroutine 时的旧 Context,而不是最新派生的那个。
- 错误写法:
go func() { doWork(ctx) }()—— 如果 ctx 在外层被重新赋值,goroutine 里用的还是旧引用 - 正确写法:
go func(ctx context.Context) { doWork(ctx) }(newCtx),或把 ctx 作为参数传进 goroutine 函数 - HTTP 中间件里用
next.ServeHTTP(w, r.WithContext(newCtx)),别只改局部变量 ctx 却没注入到 *http.Request - log 包若支持 Context(如
log/slog),优先用slog.With("trace_id", ctx.Value(traceKey)),而不是在每条日志里手动取值
Context 链路一旦断开,调试时根本看不到取消源头,只能靠日志埋点和 cancel 跟踪器(比如用 context.WithValue 存 cancel func 地址做标记)来定位——这点在复杂微服务调用链里特别痛。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











