有些响应不是一次写完的
大多数 HTTP handler 都是一次性返回 JSON:
json.NewEncoder(w).Encode(response)
但有些场景需要更细的控制:服务端持续推送日志,下载大文件时分块写入,长任务边执行边返回进度,或者你希望主动 flush 已写内容。Go 的 net/http 里有 ResponseController,它把一些响应控制能力以更明确的方式暴露出来。
初学阶段不需要在每个 handler 里使用它。普通 JSON API 完全不需要。但理解它能帮助你看懂流式响应和响应控制的边界。
Flush:把已写内容尽快发出去
一个简单流式响应:
func streamHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
rc := http.NewResponseController(w)
for i := 1; i <= 5; i++ {
fmt.Fprintf(w, "step %d\n", i)
if err := rc.Flush(); err != nil {
log.Printf("flush response: %v", err)
return
}
time.Sleep(time.Second)
}
}
访问:
curl http://localhost:8080/stream
你会逐步看到输出,而不是等 5 秒后一次性看到全部内容。
注意中间代理、浏览器和客户端也可能缓冲内容。服务端调用 Flush 不代表所有客户端都立即展示,但它表达了服务端希望尽快发送的意图。
监听请求取消
流式响应必须关心客户端是否断开:
func streamHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
rc := http.NewResponseController(w)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 1; i <= 10; i++ {
select {
case <-r.Context().Done():
log.Printf("client gone: %v", r.Context().Err())
return
case <-ticker.C:
fmt.Fprintf(w, "tick %d\n", i)
if err := rc.Flush(); err != nil {
log.Printf("flush: %v", err)
return
}
}
}
}
如果客户端关闭连接,r.Context() 会取消。不要让后台循环继续写一个已经没人读的响应。
写入截止时间
某些场景可以设置写入截止时间:
func slowWriteHandler(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Printf("set write deadline: %v", err)
}
fmt.Fprintln(w, "hello")
}
这类能力更偏底层,普通业务 handler 很少需要。大多数服务通过 http.Server 的 ReadTimeout、WriteTimeout、IdleTimeout 设置整体超时:
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
ResponseController 是补充,不是替代 server 级超时。
什么时候不需要它
普通 JSON API:
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "小林"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
这不需要 ResponseController。你只要设置响应头、状态码、编码 JSON 即可。过度使用底层控制会让简单 handler 变复杂。
适合使用的场景是:流式输出、服务端事件、长轮询进度、需要主动 flush 的下载或日志查看。只要不是这些场景,先保持简单。
一个简化的进度接口
假设后台任务需要把进度返回给命令行客户端,可以写:
func progressHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
rc := http.NewResponseController(w)
steps := []string{
"prepare files",
"upload assets",
"write database",
"done",
}
for i, step := range steps {
select {
case <-r.Context().Done():
log.Printf("progress canceled: %v", r.Context().Err())
return
default:
}
fmt.Fprintf(w, "%d/%d %s\n", i+1, len(steps), step)
if err := rc.Flush(); err != nil {
log.Printf("flush progress: %v", err)
return
}
time.Sleep(500 * time.Millisecond)
}
}
这种接口不适合所有前端页面,但对内部工具、部署脚本、长任务调试很实用。客户端能持续看到进度,而不是等到全部完成。
需要注意,响应一旦开始写出,状态码基本就确定了。不要在已经输出几行后再尝试返回一个 JSON 错误。流式接口的错误模型要提前设计,比如最后输出 error: ...,或者让客户端根据连接中断判断失败。
测试流式接口的基本思路
流式接口比普通 JSON API 难测一些,但仍然可以做基本验证。比如确认 handler 至少写出了几行内容:
func TestProgressHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/progress", nil)
rec := httptest.NewRecorder()
progressHandler(rec, req)
body := rec.Body.String()
if !strings.Contains(body, "prepare files") {
t.Fatalf("body = %q", body)
}
if !strings.Contains(body, "done") {
t.Fatalf("body = %q", body)
}
}
这个测试不验证真实网络 flush 行为,但能保护输出格式和基本流程。真正的流式行为可以在少量集成测试或手工验证里检查。入门阶段不要为了测试 Flush 把代码弄得特别复杂,先把可测试的业务部分拆出来。
例如把步骤生成逻辑独立成函数,handler 只负责写出:
func BuildSteps() []string {
return []string{"prepare files", "upload assets", "write database", "done"}
}
这样核心规则可以普通测试,HTTP 流式部分保持薄。
小结
http.ResponseController 让 Go HTTP handler 可以更明确地控制响应,比如 Flush 和写入截止时间。它适合流式响应和需要细粒度控制的场景。
写流式响应时要同时关注三件事:及时 flush、监听 r.Context() 取消、设置合理超时。普通 JSON API 不需要这些复杂度。知道什么时候不用,也是入门的重要部分。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。