http.Flusher是Go标准库专为流式响应设计的核心接口,需显式类型断言并配合context.Done()监听,否则易导致数据滞留、连接假活及goroutine泄漏。

http.Flusher 是处理耗时长连接的核心接口,不是“语言学习技巧”,而是 Go 标准库中明确设计用于流式响应的机制。直接用错地方或忽略 context.Done() 监听,会导致连接假活、资源泄漏、客户端收不到数据。
为什么不能只靠 goroutine + time.Sleep 模拟长连接
很多人误以为启动一个 goroutine 循环写入、加 time.Sleep 就算实现了长连接。实际问题很具体:
– http.ResponseWriter 默认带缓冲,不显式 Flush(),数据卡在服务端内存里不出去
– 客户端断开后,goroutine 还在跑,Write 会阻塞或返回 broken pipe 错误但没人处理
– 没绑定 req.Context(),无法感知连接中断,也无法配合超时或取消
必须把 http.ResponseWriter 转成 http.Flusher
只有实现了 http.Flusher 接口的底层 writer 才能强制刷出数据。标准 http.Server 的 responseWriter 在多数情况下支持,但需显式类型断言:
if f, ok := w.(http.Flusher); ok {
f.Flush()
} else {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}- 不要跳过
ok判断 —— 某些中间件(如 gzip handler)或测试 mock 可能不实现该接口 - 每次写完一段数据后调用
f.Flush(),不是只在结尾调一次 - 如果用了
io.WriteString或json.NewEncoder,记得它们不自动 flush,要手动补
监听 context.Done() 才算真正可靠
HTTP 请求上下文会在客户端断开、超时或服务端主动关闭时触发 Done()。不检查它,goroutine 就会变成僵尸:
立即学习“go语言免费学习笔记(深入)”;
for range time.Tick(2 * time.Second) {
select {
case <-req.Context().Done():
log.Println("client disconnected:", req.Context().Err())
return
default:
fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339))
f.Flush()
}
}-
req.Context().Done()是 channel,只能被 select 监听,不能用if轮询 - 错误值可通过
req.Context().Err()获取,常见为context.Canceled或context.DeadlineExceeded - 别在循环里漏掉
default分支 —— 否则可能因 flush 失败卡死
服务端超时配置必须和流式逻辑对齐
默认的 WriteTimeout 会覆盖整个响应生命周期,而流式响应是持续写的。若设了 WriteTimeout: 10 * time.Second,哪怕每 2 秒 flush 一次,第 11 秒也会被 kill。
- 关掉
WriteTimeout,改用IdleTimeout控制空闲时长(比如 60 秒没新数据才断) -
ReadTimeout仍建议保留(防恶意慢请求),但它不影响响应阶段 - 如果用
http.TimeoutHandler包裹 handler,它会强制应用全局超时,与流式逻辑冲突,应避免
实际中最容易被忽略的,是 Flush() 调用时机和 context.Done() 的 select 结构 —— 它们不是可选优化,而是长连接能真正“活着”的最小契约。


















