http客户端超时必须显式设置,否则默认不超时;推荐用http.client.timeout统一控制总超时,需配合context.withtimeout的正确使用及errors.is(err, context.deadlineexceeded) || (neterr, ok := err.(net.error); ok && neterr.timeout())双重判断超时错误。

HTTP客户端超时设置必须显式指定,否则默认不超时
Go 的 http.Client 默认没有连接、读写超时,一旦后端卡住或网络异常,Do() 会无限等待。这不是 bug,是设计选择——让你自己决定什么算“超时”。
常见错误现象:http: server closed idle connection 看似服务端问题,实则是客户端没设超时,等到了服务端主动断连;或者协程堆积、goroutine 泄漏。
- 用
http.DefaultClient直接发请求?危险。它底层的Transport没设超时 - 正确做法:构造自定义
http.Client,通过Timeout字段统一控制(推荐)或分别设Transport的.DialContext、ResponseHeaderTimeout等 -
Timeout是总超时,涵盖 DNS 解析、连接、重定向、TLS 握手、请求发送、响应读取全过程;若需更细粒度控制(比如只限制读响应时间),得配Transport
client := &http.Client{
Timeout: 10 * time.Second,
}区分 context.WithTimeout 和 http.Client.Timeout,别混用
两者都能“让请求停下”,但作用层级和触发时机不同。混用可能掩盖真实问题,甚至导致超时逻辑失效。
使用场景:需要在请求中途取消(比如用户关闭页面、前端 abort)、或与其他操作共用同一个超时上下文时,用 context.WithTimeout;仅控制单次 HTTP 调用生命周期,用 Client.Timeout 更直接。
立即学习“go语言免费学习笔记(深入)”;
-
Client.Timeout在Do()内部自动创建 context,并封装了所有阶段的 cancel 逻辑 -
context.WithTimeout是你传给Do()的,它只管“这个 context 什么时候取消”,不干预底层连接行为;如果Client.Timeout已设,它可能比你传的 context 更早触发 - 典型坑:同时设了
Client.Timeout = 5s又传context.WithTimeout(ctx, 10s),实际还是 5s 超时,但错误堆栈里显示的是context deadline exceeded,容易误判
超时错误类型不是 net.Error,而是 context.DeadlineExceeded
很多人用 errors.Is(err, context.DeadlineExceeded) 判断超时,这没错;但以为所有超时都走这条路,就错了——有些底层错误(如 TLS 握手超时)会包装成 net.OpError,而 OpError.Timeout() 才返回 true。
性能影响:频繁用 errors.As() 或反射判断错误类型,开销不大,但逻辑写错会导致超时被当成其他错误吞掉(比如重试非超时错误)。
- 最稳妥的判断方式:
errors.Is(err, context.DeadlineExceeded) || (netErr, ok := err.(net.Error); ok && netErr.Timeout()) - 不要只检查
err != nil就重试——DNS 失败、连接拒绝、TLS 协议错误都不是超时,重试只会放大压力 -
http.Client的重试必须手动实现,它本身不重试;超时错误一般不该重试,除非明确知道是偶发网络抖动
Server 端读请求体超时容易被忽略,ReadTimeout 已废弃
很多人只关注客户端超时,却忘了服务端也可能卡在读 body 上——比如上传大文件时客户端发得很慢,或恶意慢速攻击。Go 1.8+ 已弃用 http.Server.ReadTimeout,改用 ReadHeaderTimeout + ReadTimeout 的组合不再安全。
兼容性影响:用旧字段在新版本里会被静默忽略,导致你以为设了超时,其实没生效。
- 正确姿势:用
http.Server.ReadHeaderTimeout控制从连接建立到 header 读完的时间;再用http.MaxBytesReader限制 body 大小,或在 handler 中用ctx.Done()配合time.AfterFunc主动中断读取 - 别依赖
Request.Body.Close()自动清理——它不终止底层连接读取,只是标记“我不再读了” - 如果你用
gin或echo,它们的中间件超时通常只包住 handler 执行,不覆盖 body 读取过程,仍需额外防护
实际写的时候,最容易漏掉的是服务端读 body 的保护,以及错误类型的双重判断。这两处不补上,线上遇到慢连接或恶意请求,第一反应往往是“为什么超时没起作用”。










