context.withtimeout 是首选方案,因其自动创建带截止时间的子 context 并安全传播取消信号;需调用 cancel() 防泄漏,且 goroutine 必须主动监听 ctx.done()。

context.WithTimeout 为什么是首选方案
Go 原生并发取消靠 context.Context,不是自己搞 channel 或 flag。超时场景下,context.WithTimeout 是最直接、最安全的方式——它自动创建带截止时间的子 context,并在超时后关闭其 Done() channel,所有监听该 context 的 goroutine 都能感知并退出。
别用 time.After 单独控制超时再关 channel:它不传播取消信号,下游 goroutine 可能还在跑;也别手动写 timer + select,容易漏 defer 或 panic 后没清理。
-
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)—— 超时后自动触发cancel(),且保证只调一次 - 必须调用
cancel()(哪怕提前返回),否则可能泄漏 timer 和 goroutine - 超时时间从调用
WithTimeout开始算,不是从任务启动开始;如需“任务执行满 X 秒就停”,得在任务内部用time.Now()计时或换context.WithDeadline
goroutine 内部如何响应 cancel 信号
光传 context 进去没用,goroutine 必须主动检查 ctx.Done()。常见错误是只在开头 check 一次,或者只在阻塞操作前 check,结果 I/O 或计算卡住就彻底失联。
正确做法是:把 ctx.Done() 塞进关键 select 分支,尤其在可能阻塞的地方(HTTP 请求、channel 收发、数据库查询)。
- HTTP client 要传
ctx:用http.NewRequestWithContext(ctx, ...),再交给client.Do() - 数据库查询(如
sqlx或database/sql)支持QueryContext/ExecContext,别用老接口 - 自定义循环里加
select { case ,避免死算 - 注意:
ctx.Err()在Done()关闭后才非 nil,不要在 select 外直接判断ctx.Err() == context.DeadlineExceeded来替代 select
WaitGroup + context 混用时的典型坑
很多人用 sync.WaitGroup 等所有 goroutine 结束,但忘了 context 取消后,部分 goroutine 可能已提前 return,导致 wg.Done() 没执行完就调了 wg.Wait(),死锁或 panic。
根本问题在于取消和完成通知的时机错位。解决思路不是禁用 WaitGroup,而是确保每个 goroutine 无论是否因 cancel 返回,都执行 defer wg.Done()。
- 必须把
wg.Done()放在 goroutine 最外层defer,而不是逻辑块末尾 - 如果 goroutine 内部有多个 return 路径,不靠 defer 就极易遗漏
- 别在主 goroutine 里等
ctx.Done()再调wg.Wait():应该先wg.Wait(),再统一处理结果或错误 - 更稳妥的做法是用
errgroup.Group(官方 x/sync),它内置 context 支持和 goroutine 错误聚合,省去手写 wg + ctx 组合逻辑
第三方库不支持 context 怎么办
有些老库(比如某些 Redis 客户端旧版、自研 SDK)没提供 xxxContext 方法,也没暴露底层连接的 cancel 接口。这时候硬加 timeout 很难真正中断 I/O,只能“假装取消”。
实际效果取决于库是否用可中断的系统调用(如带 deadline 的 socket)。若底层是阻塞 read/write,超时后 goroutine 仍卡在 syscall,只是主逻辑不再等待它。
- 优先升级库版本,查文档确认是否有 context 支持(例如
go-redis/redis/v9全面支持,v8不支持) - 实在不能升级,可用
runtime.Goexit()强制退出 goroutine?不行,这是危险操作,会跳过 defer,破坏资源释放 - 折中方案:用
chan struct{}+select包裹调用,超时后不再读取返回值,但要接受 goroutine 泄漏风险 - 最务实的做法:记录日志、报警,并推动上游修复——不支持 context 的库,在 Go 生态里本质就是半残废
超时取消不是加个 timer 就完事,核心是取消信号能否穿透整个调用链。任何一环没响应 ctx.Done(),就等于留了个定时炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











