Go HTTP ResponseController 入门:更明确地控制响应写入

本文讲解 Go HTTP ResponseController 的基本使用场景,包括 Flush、写入截止时间和流式响应,让初学者理解响应控制边界。

有些响应不是一次写完的

大多数 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.ServerReadTimeoutWriteTimeoutIdleTimeout 设置整体超时:

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 不需要这些复杂度。知道什么时候不用,也是入门的重要部分。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页