context.WithCancel 是最直接的取消方式,返回可取消 Context 和 cancel 函数,调用后者立即关闭 ctx.Done();必须配对调用 cancel()(推荐 defer),且需检查 ctx.Err() 区分取消原因。

context.WithCancel 是最直接的取消方式
当需要手动触发取消时,context.WithCancel 是首选。它返回一个可取消的 Context 和一个 cancel 函数,调用后者会立即关闭 ctx.Done() 的 channel。
常见错误是忘记调用 cancel() —— 这会导致 goroutine 泄漏,尤其在循环或高频创建 context 的场景中。
- 每次调用
context.WithCancel都必须配对调用cancel(),建议用defer cancel()(除非明确需延迟取消) - 不要把
cancel传给不可信的第三方函数,它可能被意外调用 -
ctx.Done()关闭后,再次调用cancel()是安全的,但无实际效果
ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 确保退出前清理 go func() { select { case <-time.After(2 * time.Second): fmt.Println("task done") case <-ctx.Done(): fmt.Println("task canceled:", ctx.Err()) // context.Canceled } }()
time.Sleep(1 * time.Second) cancel() // 主动触发取消
select + ctx.Done() 必须配合 error 检查
仅监听 ctx.Done() 不足以判断是否真因取消退出——还需检查 ctx.Err(),否则无法区分是超时、取消,还是父 context 被关闭。
典型误用:在 select 中只读 就直接返回,不校验 ctx.Err(),导致日志或错误处理逻辑错判原因。
立即学习 “go 语言免费学习笔记(深入)”;
-
ctx.Err()在ctx.Done()关闭后才返回非 nil 值;未关闭时为nil - 如果父 context 已结束(如
WithDeadline到期),子 context 的Err()可能是context.DeadlineExceeded,不是context.Canceled - HTTP handler 中常需将
ctx.Err()映射为具体 HTTP 状态码 ,比如context.Canceled→ 499(Client Closed Request)
goroutine 中使用 context.Value 要格外谨慎
context.WithValue 适合传入请求范围的 ** 不可变元数据 **(如 trace ID、user ID),但绝不该用来传递业务参数或可变状态。
容易踩的坑是把 context.Value 当成“全局变量”替代参数传递,结果导致函数签名不透明、单元测试困难、静态分析失效。
- 值类型必须是可比较的(如
string、int、自定义 struct),且 key 类型推荐用未导出的私有类型,避免冲突 - 不要用
string字面量当 key,例如ctx = context.WithValue(ctx, "user_id", 123)—— 应定义type ctxKey string; const userIDKey ctxKey = "user_id" - 高并发下
WithValue有轻微性能开销(需拷贝 map),纯性能敏感路径应避免嵌套多层
http.Server 的 Shutdown 已自动集成 context 取消
Go 1.8+ 的 http.Server.Shutdown() 内部使用 context.Context 等待活跃连接关闭,无需手动包装。
常见误区是自己再套一层 WithTimeout 并监听 Done() 来“控制 Shutdown”,反而干扰默认行为,甚至导致连接被强制中断。
- 直接调用
srv.Shutdown(ctx)即可,传入带超时的 context 是合理做法,但别在外部额外 select 监听 - Shutdown 不会取消正在执行的 handler,只阻止新请求;handler 内仍需自行检查
r.Context().Done() - 若 handler 使用了 long poll 或 streaming,务必在循环中持续检查
ctx.Err() == nil,否则无法响应 Shutdown
并发任务取消真正难的不是调用 cancel(),而是确保每个分支、每层封装、每次 I/O 都对 ctx.Done() 做响应 —— 尤其是第三方库是否尊重 context,往往得看文档或源码。