context.withtimeout 是起点而非终点,需配合 cancel 函数使用,defer cancel() 不够稳妥,应显式调用;http 超时需用 withcontext 构造请求并禁用 client.timeout;server 层须配置 read/write/idletimeout,handler 内 ctx 应基于 r.context() 派生并透传至所有阻塞操作。

context.WithTimeout 是起点,不是终点
直接写 context.WithTimeout(ctx, 5*time.Second) 只是开始,不是加完就自动生效。它返回一个新 ctx 和一个 cancel 函数,两者必须配合使用——漏掉 cancel(),底层 time.Timer 就不会释放,高频服务跑几天后会悄悄多出一堆 goroutine。
- 必须用
defer cancel(),但要注意:如果 handler 里提前return或 panic,defer可能没机会执行;更稳妥的是在每个出口前显式调用cancel() -
WithTimeout的第二个参数是持续时间(如3*time.Second),不是时间戳;别和WithDeadline混用 - 同一个
ctx不能复用于多个独立请求——一个超时会 cancel 所有共享它的操作
HTTP 请求超时为什么有时不生效
常见现象是:设了 2 秒超时,但请求卡在 DNS 解析或 TCP 连接阶段,等了 10 秒才返回错误。根本原因在于,context 默认无法中断底层系统调用(尤其在 Windows 或旧版 Go),而 http.Client 的 Timeout 字段又只管“总耗时”,不支持中途取消。
- 正确姿势:用
http.NewRequestWithContext(ctx, ...)构造请求,再传给client.Do();同时把client.Timeout设为0,避免和 context 冲突 - Go 1.19+ 可启用
GODEBUG=netdns=cgo让 DNS 解析响应 cancel,但依赖 cgo;更通用的是配置Transport.DialContext和Transport.TLSHandshakeTimeout - 别指望 context 能“立刻断开 socket”——它保证的是
Do()返回后及时退出,不是阻塞点的即时中断
Handler 里用 context 控制业务逻辑,但别漏掉 Server 层
只在 handler 函数里套 context.WithTimeout 是不够的。如果客户端已断开连接,而你的 handler 还在读 request.Body 或写响应,goroutine 就会卡住泄漏。
-
http.Server必须显式设置ReadTimeout(建连到读完 header)、WriteTimeout(从接收 request 到写出完整 response)、IdleTimeout(keep-alive 空闲时长) - Handler 内部的超时 context 应该基于
r.Context()派生,而不是context.Background(),确保能继承请求生命周期 - 所有可能阻塞的操作都要透传这个 ctx:比如用
db.QueryContext(ctx, ...)而不是db.Query(...),用client.Do(req.WithContext(ctx))而不是忽略 ctx
别用 time.After 模拟超时
写 select { case 看似简单,但它不会通知下游 HTTP 客户端或数据库驱动停止工作,只是你自己跳出了 select。结果是:请求还在发、连接还在占、goroutine 还在跑。
-
time.After无法取消,也无法传播取消信号;context.WithTimeout才是唯一能同步关闭Done()channel 并触发所有监听者退出的机制 - 测试中避免用
context.WithTimeout(ctx, 1*time.Nanosecond),timer 精度和调度不确定性会导致测试偶然失败 - 循环中不要反复创建新 timeout context——每次
WithTimeout都启一个 timer,短周期循环会造成资源浪费
真正难的不是写对那一行 ctx, cancel := context.WithTimeout(...),而是确保整个调用链都尊重它:从 server 配置、到 handler、再到 DB 驱动和 HTTP client,每一层都得主动检查 ctx.Done(),否则超时就只是个摆设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











