长连接超时重置不能只靠http.Client.Timeout,因其控制整个请求生命周期,而流式响应需在每次读取后调用net.Conn.SetReadDeadline更新读超时;必须绕过http.Response抽象层,直控底层连接或改用net.Conn+bufio.Reader手写HTTP流。

长连接超时重置为什么不能只靠 http.Client.Timeout
因为 http.Client.Timeout 是整个请求的总耗时上限,包括 DNS 解析、连接建立、TLS 握手、写请求、读响应——它一旦触发就直接取消整个请求,无法在连接已建立、正在流式读取时“续命”。真正需要重置的是底层 net.Conn 的读/写超时,尤其是服务端持续推送数据(如 SSE、WebSocket 升级前的 keep-alive 流)时,必须在每次收到数据后调用 SetReadDeadline 更新。
如何用 http.Transport 控制底层连接的读超时
关键在于替换默认 Transport,自定义 DialContext 和 ResponseHeaderTimeout 只是辅助;核心是接管连接后的读操作。常见错误是以为设置 IdleConnTimeout 或 KeepAlive 就能解决流式超时——它们管的是空闲连接复用,不干预活跃连接的读行为。
- 必须使用
http.Transport的RoundTrip自定义逻辑,或更直接地:用http.NewRequestWithContext传入带 cancel 的 context,再手动读响应体并循环调用conn.SetReadDeadline -
Response.Body底层是io.ReadCloser,但它的Read不暴露net.Conn,所以得用http.Transport的GetConn钩子或更稳妥的方式:用net/http/httputil的DumpRequestOut不适用,应直接透传连接 - 推荐做法:不用
http.DefaultClient,而是用自定义Transport+ 包装Response.Body,在每次Read前更新 deadline
一个可工作的自动重置读超时的 io.ReadCloser 包装器
这不是装饰器模式的炫技,而是绕过 http.Response 抽象层、直控连接的必要手段。重点不是“怎么写”,而是“为什么必须这样写”:Go 的 http 包把连接生命周期和业务读取耦合太紧,不介入底层就无法重置。
type ResettableBody struct {
reader io.Reader
conn net.Conn
timeout time.Duration
}
<p>func (rb *ResettableBody) Read(p []byte) (n int, err error) {
if rb.conn != nil {
// 每次读前重置读超时,单位:秒
deadline := time.Now().Add(rb.timeout)
rb.conn.SetReadDeadline(deadline)
}
return rb.reader.Read(p)
}</p><p>func (rb *ResettableBody) Close() error {
if closer, ok := rb.reader.(io.Closer); ok {
return closer.Close()
}
return nil
}
使用时需从 response.Body 提取原始 net.Conn,这要求你用 http.Transport 的 IdleConnTimeout 等参数确保连接未被提前关闭,并在 RoundTrip 后手动保存 conn 引用——实际中更可靠的做法是改用 net/http 的底层 net.Conn 直连,跳过 http.Client。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
立即学习“go语言免费学习笔记(深入)”;
真正稳定的方案:放弃 http.Client,用 net.Conn + bufio.Reader 手写 HTTP 流
当需要精确控制每个字节的超时行为(比如反向代理、SSE 客户端、长轮询心跳),http.Client 的抽象反而成为障碍。Go 标准库的 net/http 在设计上就不是为“无限期流式响应”优化的。
- 用
net.Dial建立连接,手动写GET请求头,用bufio.NewReader(conn) - 读状态行和 header 后,进入循环:
reader.ReadSlice('\n')或reader.ReadBytes('\n'),每次成功后调用conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 遇到
io.EOF或net.ErrClosed才退出,其他错误(如net.OpError中的 timeout)可触发重连逻辑 - 注意:HTTP/2 会破坏这种模型,所以必须显式禁用:
transport.ForceAttemptHTTP2 = false,且服务端不能返回h2
复杂点不在代码长度,而在于 HTTP 协议边界处理——比如 chunked encoding 的解析、header 大小限制、连接关闭信号识别。这些细节一旦漏掉,就会出现“看似连接活着,实则服务端已断开”的静默失败。

















