长轮询与Keep-Alive无关,前者依赖服务端延迟写响应体并手动flush header,后者仅复用TCP连接;需配置Nginx proxy_read_timeout、分层控制客户端超时,并全程透传context以可靠取消请求。

长轮询不是“Keep-Alive”,别被 Transport 配置带偏
Go 的 http.Transport 默认开启 Keep-Alive,但这和长轮询完全无关——前者复用 TCP 连接发多个请求,后者是单个请求卡住不返回,等服务端有数据才写响应体。很多人一上来就调 MaxIdleConnsPerHost 或 IdleConnTimeout,结果发现客户端还是频繁重连,纯属方向错误。
- Keep-Alive 是 HTTP/1.1 的连接复用机制,长轮询是应用层协议设计,靠的是「请求挂起 + 响应延迟写出」
- 服务端 handler 必须显式控制响应时机:先写 header(
w.WriteHeader),再 flush(w.(http.Flusher).Flush()),最后等条件满足才写 body - 若用反向代理(如 Nginx),默认
proxy_read_timeout是 60 秒,超时会主动断开连接,导致长轮询提前失败
服务端 handler 必须手动 flush header 才算真正“挂起”
只写 fmt.Fprintf(w, "...") 不够。Go 的 HTTP server 默认缓冲响应头和响应体,直到 handler 返回或缓冲区满才发出去——这会让客户端根本收不到初始响应,也就无从进入“等待 body”状态。
- 正确流程:
w.WriteHeader(http.StatusOK)→w.(http.Flusher).Flush()→ 等待事件 → 写 body → 返回 - 缺少
Flush(),Wireshark 里看不到HTTP/1.1 200 OK包,客户端一直在 CONNECTING 状态 - 如果 handler 用了
log.Print或其他阻塞操作,可能在 flush 前就 panic,导致连接泄漏;务必用defer关闭资源,但别 deferresp.Body.Close()(那是客户端的事)
客户端超时控制必须分层,http.Client.Timeout 在长轮询里基本失效
http.Client.Timeout 是整个请求生命周期上限,但长轮询中服务端只要开始写响应(哪怕只写一个字节),Go 就会重置读取计时器,导致你设的 5 秒超时变成无效摆设。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 该用
Transport.ResponseHeaderTimeout:限制从建连到收到 status line 和 headers 的时间(防服务端卡死在逻辑里不发 header) - 真正等数据的阶段,要用
context.WithTimeout包裹io.ReadFull或json.NewDecoder(body).Decode()的调用,且必须透传 context 到每个 I/O 操作 - 别用
ioutil.ReadAll(resp.Body)—— 它不感知 context,一旦服务端不写完,就会永远卡住
取消长轮询请求不能靠 resp.Body.Close(),必须用 context.CancelFunc
调 resp.Body.Close() 只是关闭底层连接,对正在阻塞的 json.Decoder.Decode() 或 io.Copy() 几乎没影响。真要中断,唯一可靠方式是 cancel 绑定在 request 上的 context。
立即学习“go语言免费学习笔记(深入)”;
- 构造 request 时必须用
http.NewRequestWithContext(ctx, ...),不能用http.NewRequest() - client.Do() 返回后,所有后续读操作(包括 decode、read、copy)都要检查
ctx.Err(),否则 cancel 不生效 - 常见翻车点:goroutine 里启动请求但没把 ctx 传进去,或者用了第三方库(如
fasthttp)却不支持 context
长轮询真正的复杂点不在 Go 语法,而在整个链路的协同:服务端要能 hold 住连接不超时,中间件(Nginx / CDN / LB)得放行长连接,客户端得用对 context 层级,还要防住 goroutine 泄漏和 channel 死锁。少一环,就变成“看着像长轮询,实则每秒都在新建 TCP”。

















