当需要手动控制 goroutine 生命周期时应使用 context.WithCancel,它返回可取消的 ctx 和 cancel 函数,调用 cancel() 后所有基于该 ctx 的阻塞操作(如 select 中的)将立即返回。
当需要手动控制一个 goroutine 的生命周期时,context.WithCancel 是最直接的选择。它返回一个可取消的 ctx 和一个 cancel 函数,调用 cancel() 后,所有基于该 ctx 的阻塞操作(如 select 中的 )会立即解除阻塞。
常见错误是:在 goroutine 内部调用 cancel() 后忘记 return,导致后续代码继续执行;或者把 cancel 函数传进多个 goroutine 并发调用,引发 panic(panic: sync: negative WaitGroup counter 类错误虽不直接相关,但多处调用 cancel 属于逻辑误用)。
cancel()
cancel()
cancel(),除非你明确知道取消时机且能保证只调一次ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 关键:避免泄漏go func() { select { case <-time.After(2 * time.Second): fmt.Println("done") case <-ctx.Done(): fmt.Println("canceled:", ctx.Err()) // context canceled } }()
time.Sleep(1 * time.Second) cancel() // 此时子 goroutine 会退出
Go 标准库的 http.Client 所有 Do 方法都接受 *http.Request,而 http.NewRequest 支持传入 context.Context —— 这个 ctx 会绑定到请求的整个生命周期,包括 DNS 解析、连接建立、TLS 握手、读响应体等环节。
不传 context 或传 context.Background() 意味着请求不会响应外部取消信号;传 context.WithTimeout 则可在超时时自动中断底层连接,避免 goroutine 挂起。
context.TODO() 替代真实上下文,尤其在 HTTP 客户端场景*http.Request),用 req.Context() 获取其自带 context,而非新建http.Transport 时,确保未覆盖 CancelRequest(旧版)或依赖 RoundTripContext(1.13+ 默认启用)ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()req, _ := http.NewRequestWithContext(ctx, "GET", "https://www./link/46b315dd44d174daf5617e22b3ac94ca", nil) client := &http.Client{} resp, err := client.Do(req) if err != nil { // 可能是 context deadline exceeded if errors.Is(err, context.DeadlineExceeded) { log.Println("request timed out") } }
context.Value 设计初衷是传递**请求范围的元数据**(如 trace ID、用户身份标识、请求标记),不是用来替代函数参数或结构体字段的。它的 key 是 interface{},类型安全全靠开发者自觉,且查找是线性遍历,性能差;更严重的是,一旦 value 被覆盖(比如中间件重复 WithValue 同一 key),上游逻辑可能静默失效。
典型翻车场景:用 context.WithValue(ctx, "user_id", id) 传用户 ID,结果下游某层又用同一字符串 key 存了别的东西,导致鉴权失败却无报错。
type userIDKey struct{}),避免字符串 key 冲突type userKey struct{}
ctx = context.WithValue(ctx, userKey{}, &User{ID: 123})
// 取值时强制类型断言,避免静默失败
if u, ok := ctx.Value(userKey{}).(*User); ok {
fmt.Println(u.ID)
}
context.WithTimeout 是基于当前时间 + 持续时间推算截止时间,context.WithDeadline 直接指定绝对时间点。二者底层都生成 timer,但语义和误差来源不同:前者受系统时间跳变(如 NTP 校正)影响可能提前或延后触发;后者在系统时间被大幅调整时也可能异常,但更适合作为“必须在某个钟表时间前完成”的约定(例如定时任务调度)。
实际开发中绝大多数情况用 WithTimeout 更自然;只有当你需要对齐外部时间系统(如和数据库事务 deadline 对齐、或配合 cron 时间点)才考虑 WithDeadline。
time.Time 给 WithDeadline,返回的 context 会立刻 Done()
WithTimeout(0) 等价于立即取消,不是“不限时”
真正容易被忽略的是:无论用哪个,一旦 context 被 cancel,其 Err() 返回值是不可重用的——多次调用 ctx.Err() 没问题,但不能拿这个 error 做 switch 分支判断(因为它是指针,比较要用 errors.Is 或 == 配合预定义变量)。