
本文介绍如何在 Go 的 HTTP 代理服务中正确处理 Chunked Transfer-Encoding 响应,解决 io.Copy 导致的延迟刷新问题,通过手动调用 Flush() 实现秒级实时流式输出。
本文介绍如何在 go 的 http 代理服务中正确处理 chunked transfer-encoding 响应,解决 `io.copy` 导致的延迟刷新问题,通过手动调用 `flush()` 实现秒级实时流式输出。
在构建反向代理或中间层 HTTP 服务时,若上游服务器使用 Transfer-Encoding: chunked 持续推送数据(例如每秒返回当前时间),下游服务需确保响应同样以流式方式实时透传——而非累积缓冲后一次性发送。默认的 io.Copy 会尽可能填满内部缓冲区再写入,且不会自动触发 http.Flusher.Flush(),导致客户端(如 curl)需等待整个响应结束才收到全部内容,严重破坏流式语义。
关键在于:Go 的 http.ResponseWriter 若支持流式输出,必须显式实现 http.Flusher 接口并定期调用 Flush()。标准 io.Copy 无此逻辑,因此需自定义流式拷贝函数。
以下是一个生产可用的 flushCopy 实现,它在每次成功写入后主动刷新:
func flushCopy(dst io.Writer, src io.Reader) (int64, error) {
buf := make([]byte, 8*1024) // 8KB 缓冲,平衡性能与延迟
flusher, ok := dst.(http.Flusher)
if !ok {
return 0, fmt.Errorf("destination does not implement http.Flusher")
}
var written int64
for {
n, err := src.Read(buf)
if n > 0 {
nw, ew := dst.Write(buf[:n])
written += int64(nw)
if ew != nil {
return written, ew
}
if nw != n {
return written, io.ErrShortWrite
}
flusher.Flush() // ✅ 每次写入后立即刷新,保障实时性
}
if err == io.EOF {
break
}
if err != nil {
return written, err
}
}
return written, nil
}
在 HTTP 处理函数中使用示例:
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// 设置必要的响应头(禁用缓存、声明分块编码)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
// 向上游服务发起请求
resp, err := http.DefaultClient.Do(r.Clone(r.Context()))
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 复制响应头(除 hop-by-hop 头外)
for key, values := range resp.Header {
if !strings.HasPrefix(key, "Content-") && key != "Transfer-Encoding" {
for _, value := range values {
w.Header().Add(key, value)
}
}
}
// 使用 flushCopy 替代 io.Copy,实现实时流式转发
_, err = flushCopy(w, resp.Body)
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
log.Printf("flushCopy failed: %v", err)
}
}
⚠️ 注意事项:
- 必须确保 w 是支持 http.Flusher 的响应器(标准 http.ResponseWriter 在 HTTP/1.1 且未关闭连接时通常支持);
- 避免在 Flush() 前设置 Content-Length,否则会禁用 chunked 编码;
- 生产环境建议添加超时控制与错误重试逻辑;
- 若上游使用 Content-Length 或短连接,无需 Flush();仅当明确依赖 chunked 流式行为时才需此方案。
通过该方案,curl -i localhost:9000 将与直接访问上游服务一样,每秒收到一个独立 chunk,真正实现低延迟、可预测的流式代理能力。











