根本原因是cancel()仅关闭done通道,不强制终止goroutine;goroutine必须主动监听ctx.done()并退出,否则调用cancel()无效。

context.WithCancel 为什么调了 cancel() 却没效果
根本原因:cancel() 只是关闭 ctx.Done() channel,不主动终止 goroutine;goroutine 必须自己监听并退出。
- 常见错误是启动 goroutine 后直接调用
cancel(),但 goroutine 内部没写select { case - 如果 goroutine 阻塞在无缓冲
chan、time.Sleep或未带default的select中,它根本收不到取消信号 - 正确做法是在所有可能阻塞的点前加
select判断:ctx.Done()优先级最高,或用time.AfterFunc+cancel()替代裸 sleep - 注意:
cancel()是幂等的,但必须确保只调一次——重复调用没问题,漏调会导致 goroutine 泄漏
HTTP 请求如何真正响应 context 超时
标准库只控制请求发起阶段(DNS、连接、首字节),不中断已建立连接的读取;要完整生效,必须组合使用。
- 必须用
http.NewRequestWithContext(ctx, ...),不能用http.NewRequest(...)后手动赋值req.Context = ctx—— 后者 Transport 完全忽略 -
http.Client默认不设置读超时,大响应体可能卡住;需额外配置Transport.ResponseHeaderTimeout和Transport.ReadTimeout - 推荐双重防护:用
io.LimitReader(resp.Body, maxBytes)限制读取量,再配合select { case 监听取消 - 错误检查要区分:用
errors.Is(err, context.Canceled)或errors.Is(err, context.DeadlineExceeded),别当成业务错误重试
WithValue 传什么、不传什么
context.WithValue 不是函数参数传递通道,而是请求元数据载体;传错类型会污染接口契约、引发隐性耦合。
- ✅ 可传:trace ID、user ID、request ID、locale、log level —— 这些中间件/工具链需要,且各层不修改
- ❌ 禁止传:订单 ID、价格、开关配置、结构体指针 —— 这些是业务逻辑参数,应显式作为函数参数,否则无法做单元测试、易被误覆盖
- key 类型建议用自定义类型(如
type userIDKey struct{}),避免字符串 key 冲突;value 尽量用不可变类型 - 深层调用链中多次
WithValue会增加内存分配,高频路径慎用;纯计算函数完全不需要 context
并发任务编排时 cancel() 忘 defer 的后果
漏掉 defer cancel() 不只是“少关一个 channel”,而是导致整个子树 context 泄漏,父 context 的 deadline 无法向下收敛。
- 若父 context 是
WithTimeout,子 context 漏cancel()会导致其Deadline()返回错误时间,影响下游超时判断 - panic 时
defer仍执行,但若cancel()放在非 defer 位置,panic 前可能跳过,造成泄漏 - 多个 goroutine 共享同一
cancel()函数时,需确保只由单一控制点调用(比如主逻辑判断失败后);并发调用不会 panic,但语义混乱 - 更隐蔽的问题:cancel 后,仍有 goroutine 在执行
defer里的清理逻辑(如写日志),此时访问已关闭资源(如 closed channel)会 panic
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











