用 context.WithTimeout 包裹 goroutine 是最可靠的方式:需在启动前创建带超时的 ctx 并传入,任务中定期检查 ctx.Err() 或用 select 等待 ctx.Done(),且所有阻塞操作(如 HTTP 请求、time.Sleep、channel 收发)都须配合 ctx。

用 context.WithTimeout 包裹 goroutine 是最可靠的方式
Go 没有内置“给某个 goroutine 设超时”的语法糖,必须靠 context 主动协作。直接在 goroutine 启动前调用 context.WithTimeout,把返回的 ctx 传进去,任务内部定期检查 ctx.Err() 或用 select 等待 ctx.Done()。
常见错误是只在入口处检查一次 ctx.Err(),结果任务已阻塞在 I/O 或计算中,无法响应取消。正确做法是:所有可能阻塞的操作(如 http.Client.Do、time.Sleep、chan 收发)都要配合 ctx。
-
http.Client必须设置Timeout字段或用http.NewRequestWithContext(ctx, ...) - 自定义 channel 操作要用
select { case - 循环中每轮都加
if ctx.Err() != nil { return },尤其长耗时计算
time.AfterFunc 和 time.Timer 不能替代 context
它们只能触发单次回调或控制某段代码执行时机,但无法向正在运行的 goroutine 传递“该停了”的信号。比如你用 time.AfterFunc(5 * time.Second, func() { cancel() }),看似能取消,但如果 goroutine 正卡在没检查 ctx 的地方(比如一个没加超时的 net.Conn.Read),它根本收不到通知。
更危险的是:如果 goroutine 已经结束,而 Timer 还没触发,cancel() 可能误关其他共享资源。所以 context 是唯一能安全穿透 goroutine 生命周期的机制。
立即学习“go语言免费学习笔记(深入)”;
记住:time.AfterFunc 适合“延后执行”,context.WithTimeout 才是“限时执行”。
并发任务组统一超时要共用同一个 context
启动多个 goroutine 处理不同子任务(比如并行调用三个微服务),又希望整体不超过 3 秒,不能每个都调用 context.WithTimeout(parent, 3*time.Second) —— 这样每个子任务都有独立的 3 秒倒计时,总耗时可能达 9 秒。
正确做法是:只调一次 context.WithTimeout,把生成的 ctx 传给所有 goroutine:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel()var wg sync.WaitGroup for , url := range urls { wg.Add(1) go func(u string) { defer wg.Done() // 所有 http 请求都用这个 ctx req, := http.NewRequestWithContext(ctx, "GET", u, nil) client := &http.Client{} resp, err := client.Do(req) if err != nil { // ctx 超时会返回 context.DeadlineExceeded if errors.Is(err, context.DeadlineExceeded) { return } } // ... }(url) } wg.Wait()
注意:cancel() 必须由主 goroutine 调用,别在子 goroutine 里乱调,否则可能提前中断其他还在跑的任务。
超时后资源清理容易被忽略
超时不是终点,而是清理起点。常见遗漏点:
- 未关闭已打开的
http.Response.Body—— 即使请求超时,Body 可能还在流式读取,不关会导致连接泄漏 - goroutine 退出前没释放持有的锁(
sync.Mutex.Unlock())、数据库连接(db.Close())、文件句柄(f.Close()) - 向 channel 发送数据时被超时中断,但 channel 是无缓冲的,发送操作会永久阻塞 —— 必须用
select+default或ctx.Done()保护
最稳妥的写法是用 defer 绑定清理逻辑,但要确保 defer 在超时路径下也能执行(比如把 defer 放在 goroutine 函数开头,而不是等整个函数结束)。










