Go实现长轮询必须先调用WriteHeader和Flush以发送响应头,否则客户端无法进入等待body状态;需用select监听dataChan、ctx.Done()和time.After三信号,禁用time.Sleep以防goroutine泄漏。

Go 实现长轮询,核心不是“等几秒再返回”,而是先发响应头、立刻刷出,再阻塞等待数据或超时——否则客户端永远卡在 pending 状态。
为什么必须调用 w.WriteHeader 和 w.(http.Flusher).Flush()
Go 的 http.ResponseWriter 默认缓冲整个响应。如果只写 body(比如 w.Write([]byte{}))或漏掉 w.WriteHeader,客户端收不到状态行和 header,就无法进入“等待 body”阶段,连接会一直挂起或最终被 Nginx 判为超时断开。
- 必须在任何读取/等待逻辑前调用
w.WriteHeader(http.StatusOK) - 紧接着调用
w.(http.Flusher).Flush()—— 注意:标准 HTTP/1.1 连接都支持http.Flusher,但 HTTP/2 不支持,生产环境需确认协议版本 - 如果 flush 后又在 timeout 分支里重复调用
w.WriteHeader,会 panic:“http: multiple response.WriteHeader calls”
如何用 select 安全等待:别碰 time.Sleep
time.Sleep 是伪长轮询:它不响应数据到达、不感知客户端断开、无法被中断,上线后必然导致 goroutine 泄漏。
- 每个请求必须创建独立的 context:
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second),不能复用全局 context - 监听三个信号源:
(服务端有新数据)、<code>(客户端断开或超时)、<code>(兜底,建议比 Nginx 的 <code>proxy_read_timeout小 5–10 秒) - channel 类型推荐
chan []byte或chan string,容量至少 100;设为 1 容易让高并发推送方卡死在发送操作上 - 推送方写 channel 时要用非阻塞 select:
select { case ch ,防止接收方已退出还拖住生产者
客户端断开、服务端推送、超时退出,三者必须共存
真实场景中,连接可能随时因网络抖动、用户关闭页面、Nginx 主动切断而中断。仅靠 time.After 不够,必须依赖 r.Context().Done()。
- 每次 handler 进入就要检查
if ctx.Err() != nil { return },尤其在Flush()之后、写 body 前 - 若用
sync.Map存活跃连接的 channel,注册/注销必须成对;handler 返回前务必清理,否则内存持续增长 - 不要把
*http.ResponseWriter保存到全局变量或结构体中——它生命周期只在 handler 内有效,且非线程安全 - Nginx 配置必须调大:
proxy_read_timeout 240,并禁用 buffering:proxy_buffering off;否则它会在 60 秒默默断连,而你的 goroutine 还在 channel 上傻等
常见踩坑点:字符串转换、超时分层、资源清理
很多长轮询服务本地能跑通,一上线就泄漏或乱码,问题往往出在细节。
-
string(counter)是经典陷阱:它把整数当 ASCII 码转,counter = 65变成"A";必须用strconv.Itoa(counter)或json.Marshal - 客户端
http.Client.Timeout对长轮询基本无效——只要服务端 flush 了 header,Go 就重置读计时器;真正起作用的是Transport.ResponseHeaderTimeout(控制建连到收到 header 的时间)和反向代理配置 - handler 中不要起 goroutine 后直接 return;也不要使用
http.TimeoutHandler,它会强制关闭连接,退化为短轮询 - 每次响应后客户端必须立刻发起下一次请求;若用 JavaScript,记得每次 new
XMLHttpRequest,避免旧实例 abort 干扰新连接
最易被忽略的是:所有阻塞点(包括 select 前、写 body 前、甚至日志打印前)都要主动检查 ctx.Err();一旦忽略,goroutine 就会永久卡住,直到进程重启。


















