长轮询需显式写状态码并刷新响应头,再通过channel阻塞等待数据;禁用time.Sleep,改用带缓冲channel(如cap=100)避免推送方阻塞。

长轮询不是 Keep-Alive,别被 Transport 默认值骗了
Go 的 http.Transport 默认开启 KeepAlive,但这只是复用 TCP 连接发多个请求——和长轮询完全无关。长轮询的关键是:一个请求卡住不返回,等服务端有数据才写响应体。
常见错误是 handler 里直接 fmt.Fprintf(w, "...") 就结束,结果 Go 缓冲整个响应,客户端收不到 header,根本进不了“等待 body”状态。
- 必须显式调用
w.WriteHeader(http.StatusOK)发状态码 - 立刻调用
w.(http.Flusher).Flush()强制刷出 header - 之后再阻塞等待数据(比如从
chan string读),等到了才写 body
服务端阻塞逻辑必须基于 channel,不能靠 time.Sleep
用 time.Sleep(10 * time.Second) 模拟等待,本地能跑通,但上线就崩——它不感知业务变化,也不释放 goroutine,更没法被推送事件中断。
真正可用的方案是让推送方和轮询方共用一个带缓冲的 channel:
立即学习“go语言免费学习笔记(深入)”;
- 定义
messages := make(chan string, 100),容量太小(如 1)会导致高并发时推送方阻塞在messages - 轮询 handler 用
msg := 阻塞读取,有数据立刻响应 - 推送逻辑(如计数器更新)只管往 channel 发:
messages ,注意别用 <code>string(counter)(那是 ASCII 转换)
客户端超时控制必须分层,Client.Timeout 基本失效
http.Client.Timeout 对长轮询没用:只要服务端开始写响应(哪怕只写一个字节),Go 就重置读取计时器,你设的 5 秒超时就成摆设。
正确做法是三层超时配合:
- 用
Transport.ResponseHeaderTimeout控制“建连到收到 header”的时间(防服务端卡死不发头) - 发起请求时用
http.NewRequestWithContext(ctx, ...),把 context 透传到底层 - 读 body 时不用
io.ReadAll,改用json.NewDecoder(body).Decode(&v)并确保每个 I/O 都检查ctx.Err();或者手动封装带 cancel 检查的读循环
Nginx 等反向代理会默默掐断连接,服务端必须兜底
生产环境几乎必过 Nginx,而它的默认 proxy_read_timeout 是 60 秒——连接空闲超过这个时间,Nginx 直接断开,你的 goroutine 却还在 channel 上傻等,造成泄漏。
所以服务端 handler 不能只依赖 channel 阻塞,还得加一层 context 超时兜底:
- 用
ctx, cancel := context.WithTimeout(r.Context(), 240*time.Second)(比 Nginx timeout 小一点) - 然后
select { case msg := - 响应头务必加
Connection: keep-alive和Cache-Control: no-cache,避免中间件缓存或误判
最麻烦的点从来不在代码怎么写,而在你本地测试时一切正常,一上 Nginx 或 ALB 就断连、超时、goroutine 泄漏——这些中间件的默认策略,才是长轮询真正的门槛。


















