需显式调用 http.flusher.flush() 实现 html 流式响应,因 responsewriter 默认缓冲;须类型断言获取 flusher,按语义块(如每列表项)flush,避免模板预渲染与 http/2 兼容问题,并确保前端能增量解析合法 html 结构。

用 http.Flusher 实现 HTML 流式响应
Go 的 http.ResponseWriter 默认带缓冲,直接 fmt.Fprintf(w, ...) 不会立刻发给浏览器——页面会“卡住”直到 handler 返回或缓冲区满。必须显式调用 Flush() 才能逐块推送 HTML。
关键前提是:底层实现支持 http.Flusher(HTTP/1.1 下标准 ResponseWriter 支持,但需类型断言)。
- 每次写入 HTML 片段后,立即执行
if f, ok := w.(http.Flusher); ok { f.Flush() } - 避免在循环中频繁 flush(如每行都 flush),建议按语义块 flush,例如每渲染一个列表项后 flush 一次
- 若 handler 返回前未 flush,所有内容仍会作为完整响应发出,但失去“流式”意义
- 注意:HTTP/2 不支持传统 flush;若启用了 HTTP/2,部分 flush 可能被静默忽略(取决于服务器配置和客户端行为)
避免模板预渲染导致内存堆积
html/template.Execute 会一次性将整个模板结果写入 ResponseWriter 的缓冲区,违背流式目标。不能把整页数据塞进一个 Execute 调用里。
- 改用
template.ExecuteTemplate分段渲染公共区块(如{{define "header"}}),但依然要配合手动 flush - 更可控的方式是:用
template.New("chunk").Parse(...)构造轻量子模板,每次只渲染一个数据单元(如单条记录),然后 write + flush - 不要传大 slice 给模板(如
tmpl.Execute(w, struct{ Items []BigStruct })),这会让模板内部遍历并缓存全部输出
结合 channel 实现“读-渲染-推送”流水线
当数据源来自数据库游标、文件扫描或外部 API 流时,需解耦读取与响应推送,防止阻塞。
- 启动一个 goroutine 持续从数据源读取(如
rows.Next()或scanner.Scan()),将解析后的结构体发到ch chan *Item - 主 handler 循环接收
ch中的 item,调用小模板或字符串拼接生成 HTML 片段,fmt.Fprintf(w, ...)后立即Flush() - 用
close(ch)通知结束;handler 中用for item := range ch自然退出 - 务必设
ch缓冲(如make(chan *Item, 100)),否则生产者可能因消费者慢而阻塞
注意 Content-Type 和前端接收逻辑
流式 HTML 前端必须能增量解析,否则仍显示空白。常见坑点:
- 必须设置
w.Header().Set("Content-Type", "text/html; charset=utf-8"),且不能设Content-Length(长度未知) - 浏览器对流式 HTML 的支持不一致:Chrome/Firefox 支持较好;Safari 对非完整标签(如只写
<div> 不闭合)可能延迟渲染或丢弃 <li>推荐在开头写一个完整可解析的骨架(如 <code>),再逐块追加内容,结尾补闭合标签 - 前端 JS 若用
fetch接收流式 HTML,需用response.body.getReader()+TextDecoder手动解析,不能依赖response.text()
流式写 HTML 最容易被忽略的是 flush 的时机和前置 HTML 结构的完整性——没 flush 就等于没发送,没合法开头就等于前端不渲染。别指望模板引擎自动帮你流式化,它天生是批处理设计。











