time.after 不能在 for 循环里反复调用,因其每次都会新建不可取消的 timer 并启动 goroutine,导致 goroutine 和 timer 堆积泄漏;正确做法是用 context.withtimeout 配合显式 cancel 或复用 timer。

Go 里超时控制不是“加个定时器就完事”,核心在于信号传播和资源清理——用错方式,time.After 在循环里会堆积 goroutine,context.WithTimeout 不调 cancel() 会泄漏 timer,HTTP 请求只设 Client.Timeout 却不传 ctx,超时根本不会触发。
为什么 time.After 不能在 for 循环里反复调用
每次 time.After 都新建一个独立的 Timer,底层启动新 goroutine 等待,且无法取消。循环中反复调用,等于不断起新 goroutine,前面的 timer 全变成僵尸,直到超时时间到才释放。
- 错误写法:
for range jobs { select { case —— 每次都重置计时,前面所有等待全作废 - 正确写法:把
time.After提到循环外,所有select共享同一个超时信号:timeout := time.After(1*time.Second); for range jobs { select { case - 适用场景:仅限单次、无嵌套、无下游传播的简单等待,比如等一个 channel 返回或本地计算兜底
- 硬伤:它不提供取消能力,也不通知下游函数“我已经不等了”,纯靠外部中断,无法释放 HTTP 连接、DB 句柄等资源
context.WithTimeout 必须配 cancel() 才算完整
context.WithTimeout 本身只是生成一个带 deadline 的 context 和一个 cancel 函数;不调 cancel(),底层 timer 不会停,context 内部的 channel 也不会被 GC,高频请求下内存缓慢上涨。
- 必须显式接收并调用:
ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second); defer cancel() - 不能丢掉
cancel:ctx, _ := context.WithTimeout(...)是典型反模式,后续无处可调 - panic 恢复后、
return前、select的default分支里,都要确保cancel()被执行 - 超时起点是
WithTimeout调用那一刻,不是你发起http.Do或启动 goroutine 的时刻——中间卡在锁或调度延迟,都会计入总耗时
HTTP 请求超时必须分层,不能只靠 Client.Timeout
http.Client.Timeout 是总生命周期超时(DNS + 连接 + 写请求 + 读响应头 + 读响应体),但它不可动态设置,且一旦配置就全局生效。线上出问题时,你根本不知道卡在哪一环。
- 推荐做法:用
context.WithTimeout控制请求粒度,再配合http.Transport分层设超时 - 关键配置项:
ProxyConnectTimeout(代理连接)、IdleConnTimeout(空闲连接)、ResponseHeaderTimeout(响应头)、ReadTimeout(读 body) - 常见坑:
Client.Timeout和WithContext(ctx)同时存在时,context 超时优先级更高;但如果没把 ctx 传给Do(),Client.Timeout就是唯一防线,而它无法区分是 DNS 慢还是 body 卡住 - 示例:
req = req.WithContext(ctx); resp, err := client.Do(req)—— 缺少这行,ctx完全无效
net.Conn 的读写超时必须每次 I/O 前重设
SetReadDeadline 和 SetWriteDeadline 设置的是绝对时间点,只对「下一次」I/O 生效,用完即失效。不是设一次就管全程。
- 每次
Read()前必须重设:conn.SetReadDeadline(time.Now().Add(5 * time.Second)) - 用
bufio.Reader时,不能只初始化时设一次;要在每次reader.Read()前手动对底层conn重设 deadline -
Write()卡住比Read()更隐蔽——可能堵在内核 buffer 或对端窗口为 0,不设写超时,goroutine 会 hang 几分钟 - 连接建立阶段也得控:用
&net.Dialer{Timeout: 5 * time.Second}替代net.DialTimeout,同时启用KeepAlive防 NAT 断连滞留
真正难的不是写出 context.WithTimeout 那一行,而是想清楚:超时信号由谁发出、谁监听、谁清理、谁感知。漏掉任一环,超时就只是日志里一句 context deadline exceeded,而 goroutine 和连接还在后台静默消耗资源。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











