
本文介绍如何使用 io.copy 将上游 http 响应直接流式传输到 http.responsewriter,实现零拷贝、低内存占用的响应转发,避免将整个响应体加载到内存中。
在构建代理型 API 或文件网关服务时,常需将第三方服务返回的响应(如图片、PDF、视频流等)原样、低延迟地透传给客户端。关键诉求是:不将完整响应体读入内存,而是边接收、边转发。此时,io.Copy 是最简洁、高效且符合 Go 语言惯用法的解决方案。
io.Copy(dst io.Writer, src io.Reader) 内部采用固定大小缓冲区(默认 32KB)循环读取 src 并写入 dst,天然支持流式处理,无需额外 goroutine 或管道协调。它与已存在的 http.Response.Body(io.ReadCloser)和 http.ResponseWriter(io.Writer)完美契合——这正是问题中提到的“已有 Reader/Writer”场景,完全不需要 io.Pipe()。io.Pipe() 适用于需要手动桥接生产者与消费者、且二者生命周期需解耦的复杂场景(如异步生成数据),在此类透传任务中反而引入不必要开销与错误风险。
以下是一个生产就绪的示例:
func proxyFileHandler(w http.ResponseWriter, r *http.Request) {
// 构造上游请求(建议使用 context 控制超时)
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/file.pdf", nil)
if err != nil {
http.Error(w, "failed to build request", http.StatusInternalServerError)
return
}
// 发起请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, "upstream request failed", http.StatusBadGateway)
return
}
defer resp.Body.Close() // 确保下游连接关闭后释放资源
// 复制关键 Header(注意:避免复制 hop-by-hop 头,如 Connection、Transfer-Encoding)
for _, h := range []string{"Content-Type", "Content-Length", "Content-Disposition", "Last-Modified", "ETag"} {
if v := resp.Header.Get(h); v != "" {
w.Header().Set(h, v)
}
}
// 流式转发主体内容
_, err = io.Copy(w, resp.Body)
if err != nil && err != io.ErrUnexpectedEOF {
// 客户端提前断连时 io.Copy 可能返回 ErrUnexpectedEOF,通常可忽略
log.Printf("copy failed: %v", err)
}
}注意事项:
- ✅ 务必调用 resp.Body.Close():防止底层 TCP 连接泄漏;使用 defer 确保执行。
- ✅ 谨慎复制 Header:仅透传语义安全的头字段;跳过 Connection、Keep-Alive、Proxy-Authenticate 等 hop-by-hop 头(HTTP/1.1 规范要求)。
- ✅ 设置超时:通过 context 防止上游无响应导致服务挂起。
- ⚠️ 避免手动设置 Content-Length:若上游使用 chunked 编码或动态生成内容,其 Content-Length 可能为空或不准确;io.Copy 会自动处理分块传输,无需显式设置。
- ⚠️ 错误处理:io.Copy 返回 io.ErrUnexpectedEOF 通常表示客户端主动断开(如页面关闭),一般无需报错;其他错误需记录并排查网络或上游问题。
综上,io.Copy 是流式 HTTP 响应转发的黄金标准——简单、可靠、内存友好。牢记其适用边界(已有 Reader/Writer)、配合恰当的 Header 管理与错误策略,即可构建高性能、健壮的反向代理逻辑。









