go http客户端默认无超时,需手动配置;推荐用context.withtimeout控制单次请求,并分别设置连接、读头等阶段超时,服务端也需配置readheadertimeout和idletimeout。

Go HTTP客户端默认不设超时,必须手动配置
Go 的 http.Client 默认没有超时限制,一旦后端卡住或网络异常,请求会无限等待,最终拖垮整个服务。这不是 bug,而是设计选择——把控制权交还给使用者。但生产环境里,不设超时等于埋雷。
关键不是“要不要超时”,而是“在哪一层设、设多长”。常见错误是只设 Timeout,却忽略底层连接和读写的分阶段控制。
-
Timeout是总超时(连接 + 读响应),简单但不够精细 - 更稳妥的做法是分别设置:
Transport.DialContext控制建连时间,Transport.ResponseHeaderTimeout控制收到 header 的等待时间,Transport.ReadTimeout(已弃用)应改用Transport.TLSHandshakeTimeout和自定义http.Transport配合上下文 - 推荐方式:用
context.WithTimeout包裹每个请求,再传给client.Do(req.WithContext(ctx))—— 这样能统一管控,且支持取消传播
使用 context.WithTimeout 处理单次请求超时
这是最常用也最灵活的方式,适用于大多数外部 API 调用场景。它不依赖 client 全局配置,每次请求可独立设定超时策略。
示例代码片段:
立即学习“go语言免费学习笔记(深入)”;
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := client.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
// 明确是超时,可打日志、降级或重试
}
return err
}
defer resp.Body.Close()
- 注意:必须调用
cancel(),否则 context 泄漏会导致 goroutine 积压 - 不要在循环中复用同一个
context.WithTimeout,每次请求都应新建 ctx -
ctx.Err()检查比判断err.Error()是否含 "timeout" 更可靠,因为底层错误类型可能变化
HTTP Server 端也要防请求卡死
很多人只关注 client 超时,却忘了 server 端同样危险:一个慢请求占着连接不释放,会耗尽 net.Listener 的文件描述符或 http.Server 的并发连接数。
- 设置
http.Server.ReadTimeout和http.Server.WriteTimeout(Go 1.8+ 支持),分别控制读请求头/体、写响应的上限 - 更现代做法是用
http.Server.ReadHeaderTimeout(仅读 header)+http.Server.IdleTimeout(空闲连接保持时间),避免大 Body 上传被误杀 - 如果 handler 内部有阻塞操作(如数据库查询、远程调用),必须自己用 context 控制,server 层的超时无法中断正在执行的 handler 逻辑
超时值不是越短越好,需结合下游 SLA 和重试策略
设成 100ms 可能降低 P99 延迟,但也可能把本可成功的请求直接判为失败;设成 30s 又失去超时意义。真实系统里,超时要和重试、熔断联动。
- 对强依赖服务(如支付回调),超时建议略大于其 P95 响应时间,并配 1 次快速重试(带 jitter)
- 对弱依赖或兜底服务(如埋点上报),可设较短超时(如 200ms),失败直接丢弃,不重试
- 避免在重试逻辑里复用同一 context,每次重试应新建带新 deadline 的 ctx
- 上线前务必用
go tool trace或 pprof 查看实际请求耗时分布,别凭感觉拍数字
超时管理真正的复杂点不在代码怎么写,而在于你是否清楚每个接口的稳定性水位、下游承诺的 SLA、以及你的服务能容忍多少失败率——这些决定了 timeout 数值本身,而不是技术选型。










