长轮询不能直接用http.handlefunc,因默认handler需函数返回才响应,而长轮询需挂起连接、等待事件后响应;必须禁用http/2、调大超时、用context控制生命周期、select监听ctx.done()与事件通道、避免goroutine泄漏,并通过flush()确保及时推送。

长轮询为什么不能直接用 http.HandleFunc 一写就跑?
因为默认的 HTTP handler 会等 handler 函数返回才发响应,而长轮询需要“挂起连接、等待事件、再响应”,必须手动控制连接生命周期。一旦 handler 返回,连接就关闭了——你还没发数据,连接先断了。
关键点在于:禁用 HTTP/2 的流式复用(它会干扰长连接),并确保连接不被中间代理(如 Nginx)或 Go 的 http.Server 默认超时机制提前掐断。
- 启动 server 时显式禁用 HTTP/2:
server := &http.Server{Addr: ":8080", Handler: mux}+server.SetKeepAlivesEnabled(true) - 设置
ReadTimeout和WriteTimeout为 0(禁用)或设为远大于预期等待时间(比如 300 秒) - Nginx 前置时,必须配
proxy_read_timeout 300;和proxy_buffering off;,否则它会在 60 秒后主动断连
如何安全地等待事件并避免 goroutine 泄漏?
每个长轮询请求都会启一个 goroutine 等待事件,但客户端可能随时关浏览器、网络中断、或超时放弃。如果只用 chan 阻塞接收,goroutine 就永远卡住,内存持续增长。
正确做法是把 context.Context 传进等待逻辑,监听 ctx.Done(),并在退出前清理资源(比如从事件订阅池中注销)。
- 用
ctx, cancel := context.WithTimeout(r.Context(), 240*time.Second)包一层,防止无限等待 - 事件通道读取必须用
select同时监听ctx.Done()和事件 channel - 若
ctx.Err() == context.Canceled,说明是客户端断开;若是DeadlineExceeded,则是服务端主动超时,都该立即 return 并调用cancel() - 不要在 handler 里直接
time.Sleep模拟等待——它阻塞整个 goroutine,且无法响应取消信号
net/http 下如何正确写响应而不触发 “http: response.WriteHeader on hijacked connection”?
长轮询不需要 hijack,也千万别调 ResponseWriter.Hijack()。99% 场景下,你只需要正常写响应体,但要确保:1)不提前调 WriteHeader;2)在首次 Write 时由 Go 自动发 status line 和 headers;3)之后可多次 Write(只要没刷新缓冲区)——但注意:HTTP/1.1 不支持服务端多次响应,所以实际只能写一次完整 body。
常见错误是:先 w.WriteHeader(200),再 w.Write([]byte{...}),这没问题;但如果中间有 log 或 panic 恢复逻辑又重复 WriteHeader,就会报那个错误。
- 始终让 Go 自动写 header:只调
w.Write(),不要显式WriteHeader - 若需自定义 status code,仅在第一次
Write前调一次w.WriteHeader(200),且确保后续无其他 WriteHeader 调用 - 响应内容建议用 JSON 流式结构(如
{"event":"msg","data":{...}}),结尾加换行便于前端按行解析 - 写完后显式
flusher, ok := w.(http.Flusher),if ok { flusher.Flush() },强制推送到客户端,避免内核缓冲延迟
客户端断连时服务端怎么感知并释放资源?
Go 的 http.ResponseWriter 不暴露底层连接状态,r.Context().Done() 是唯一可靠信号。但要注意:这个 channel 只在客户端明确关闭连接(FIN)或超时时才关闭,不是每次网络抖动都触发。
真正棘手的是“半开连接”:客户端已死,但 TCP 连接未发 FIN,服务端还傻等。这时得靠 TCP keepalive 和应用层心跳配合。
- 启用 OS 级 keepalive:
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context { ... }中对c.(*net.TCPConn)调SetKeepAlive(true)和SetKeepAlivePeriod(30 * time.Second) - 更稳妥的做法是在 handler 开头注册一个 defer 清理函数,绑定到
r.Context().Done()上,统一注销订阅、关闭 channel、记录日志 - 避免在
select中只监听事件 channel 而忽略ctx.Done()—— 这会导致 goroutine 永久泄漏,压垮服务
长轮询真正的复杂点不在“怎么发数据”,而在“怎么确认对方还在听”。所有超时、上下文、连接管理、中间件兼容性,都得围绕这个前提设计。少一个 select 分支,或漏掉一次 Flush(),都可能让前端卡住几秒甚至几十秒才重连。











