不能只用 time.After 判断超时,因为 time.After 仅触发一次超时信号,无法接收外部取消指令,导致 goroutine 无法及时终止而泄漏。

Go 里并发任务超时退出,必须用 context.Context 配合 select 监听 ctx.Done(),不能靠 time.After 单独判断,也不能在 goroutine 内部硬 sleep 后 return —— 那样无法响应外部取消,会泄漏。
为什么不能只用 time.After 判断超时
单纯在 goroutine 里写 select { case ,看似能“超时退出”,但问题很实在:
- 它不感知外部取消信号,比如父请求已中断,这个 goroutine 还会傻等到 3 秒才停
- 它无法向调用方透出真实错误原因(是超时?还是被 cancel?),
ctx.Err()才能区分context.DeadlineExceeded和context.Canceled - 如果任务本身有 I/O 操作(如
http.Client.Do、sql.DB.QueryContext),不传ctx就根本不会受控中断
context.WithTimeout 的正确创建与释放时机
超时 context 必须从上游 context 派生,且 cancel() 一定要调用,否则底层 timer 不回收,长期运行服务会累积 goroutine 泄漏:
- 别在循环里反复调用
context.WithTimeout(ctx, time.Second)—— 每次都新建 timer,旧 timer 不会被 GC - 要在 goroutine 启动前创建,且
defer cancel()放在最外层函数,不是放在 goroutine 内部 - 若需多次重试,应复用同一个带 timeout 的 context,或改用
context.WithDeadline+ 显式重置逻辑
示例片段:
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel() // 这行必须在外层,确保无论是否进 goroutine 都执行
<p>go func(ctx context.Context) {
for {
select {
case </p>
goroutine 退出时如何避免资源残留
光让 goroutine return 不够,还要确认它真停了、没卡在 channel 发送、没占着锁、没漏掉 close:
- 往无缓冲 channel 发送数据前,务必先检查
ctx.Done(),否则可能永久阻塞 - 若使用带缓冲 channel 做结果回传,发送前加
select双路判断:case ch 和case - 涉及文件、网络连接、数据库等资源,必须在
ctx.Done()分支里显式 cleanup(如conn.Close()) - 不要依赖
runtime.Gosched()或time.Sleep(0)让出调度 —— 这不是退出机制,只是干扰调度器
HTTP handler 中超时控制的典型陷阱
很多人直接在 handler 里用 context.Background() 创建子 context,这会导致整个链路取消失效:
- 必须用
r.Context()作为根 context 派生,才能继承 request 生命周期 - 下游所有可传 ctx 的 API(
http.NewRequestWithContext、client.Do、db.QueryRowContext)都要透传该 ctx - 别在 handler 里启动 goroutine 后就不管了 —— 要用
sync.WaitGroup或errgroup.Group等待完成,否则可能 response 已写出,goroutine 还在跑
最易忽略的一点:超时时间要留出余量。比如你设了 3 秒 timeout,但内部 RPC 调用用了 2.9 秒,再加一点序列化开销,很可能刚到 deadline 就被砍断,而错误日志里只显示 context deadline exceeded,看不出是哪一环拖慢的。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











