当需要手动控制 goroutine 生命周期时应使用 context.WithCancel,它返回可取消的 ctx 和 cancel 函数,调用 cancel() 后所有基于该 ctx 的阻塞操作(如 select 中的)将立即返回。

context.WithCancel 什么时候该用
当需要手动控制一个 goroutine 的生命周期时,context.WithCancel 是最直接的选择。它返回一个可取消的 ctx 和一个 cancel 函数,调用 cancel() 后,所有基于该 ctx 的阻塞操作(如 select 中的 )会立即解除阻塞。
常见错误是:在 goroutine 内部调用 cancel() 后忘记 return,导致后续代码继续执行;或者把 cancel 函数传进多个 goroutine 并发调用,引发 panic(panic: sync: negative WaitGroup counter 类错误虽不直接相关,但多处调用 cancel 属于逻辑误用)。
- 只应在发起方(父 goroutine)中调用一次
cancel() - 若子 goroutine 自行决定退出,应通过 channel 或其他信号通知父方,由父方调用
cancel() - 务必在 defer 中调用
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 会退出
HTTP 请求里怎么传 context
Go 标准库的 http.Client 所有 Do 方法都接受 *http.Request,而 http.NewRequest 支持传入 context.Context —— 这个 ctx 会绑定到请求的整个生命周期,包括 DNS 解析、连接建立、TLS 握手、读响应体等环节。
不传 context 或传 context.Background() 意味着请求不会响应外部取消信号;传 context.WithTimeout 则可在超时时自动中断底层连接,避免 goroutine 挂起。
- 不要用
context.TODO()替代真实上下文,尤其在 HTTP 客户端场景 - 如果已有 request(如 handler 中的
*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.php.cn/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 不要存关键业务数据
context.Value 设计初衷是传递**请求范围的元数据**(如 trace ID、用户身份标识、请求标记),不是用来替代函数参数或结构体字段的。它的 key 是 interface{},类型安全全靠开发者自觉,且查找是线性遍历,性能差;更严重的是,一旦 value 被覆盖(比如中间件重复 WithValue 同一 key),上游逻辑可能静默失效。
典型翻车场景:用 context.WithValue(ctx, "user_id", id) 传用户 ID,结果下游某层又用同一字符串 key 存了别的东西,导致鉴权失败却无报错。
- key 必须是自定义类型(如
type userIDKey struct{}),避免字符串 key 冲突 - 只存轻量、只读、跨多层中间件需共享的非核心数据
- 业务主数据(如订单 ID、用户对象)必须显式作为函数参数或方法 receiver 传递
type userKey struct{}
ctx = context.WithValue(ctx, userKey{}, &User{ID: 123})
// 取值时强制类型断言,避免静默失败
if u, ok := ctx.Value(userKey{}).(*User); ok {
fmt.Println(u.ID)
}
WithTimeout 和 WithDeadline 的关键区别
context.WithTimeout 是基于当前时间 + 持续时间推算截止时间,context.WithDeadline 直接指定绝对时间点。二者底层都生成 timer,但语义和误差来源不同:前者受系统时间跳变(如 NTP 校正)影响可能提前或延后触发;后者在系统时间被大幅调整时也可能异常,但更适合作为“必须在某个钟表时间前完成”的约定(例如定时任务调度)。
实际开发中绝大多数情况用 WithTimeout 更自然;只有当你需要对齐外部时间系统(如和数据库事务 deadline 对齐、或配合 cron 时间点)才考虑 WithDeadline。
- 不要混用:传入已过期的
time.Time给WithDeadline,返回的 context 会立刻Done() -
WithTimeout(0)等价于立即取消,不是“不限时” - 嵌套 timeout 时,内层 timeout 不能长于外层,否则外层 cancel 会先触发,内层 timer 成为垃圾
真正容易被忽略的是:无论用哪个,一旦 context 被 cancel,其 Err() 返回值是不可重用的——多次调用 ctx.Err() 没问题,但不能拿这个 error 做 switch 分支判断(因为它是指针,比较要用 errors.Is 或 == 配合预定义变量)。











