io.copy和http.servefile均非真零拷贝,因其目标为封装的http.responsewriter而非裸net.conn,强制走read+write用户态拷贝;真零拷贝须用file.writeto(conn)并严格满足源为os.file、目标为未包装*net.tcpconn、内核≥2.6.33等条件。

Go 里用 io.Copy 分发视频流,基本不触发零拷贝——除非你严格满足三重条件:源是 *os.File、目标是裸 *net.TCPConn、内核 ≥ 2.6.33。
为什么 io.Copy 和 http.ServeFile 都不是真零拷贝
它们看起来“直接转发”,但实际都绕不开用户态缓冲区。比如 http.ResponseWriter 是带 header 封装和 buffer 的抽象层,io.Copy(w, file) 最终调用的是 w.Write(),数据必须先从内核页缓存拷进 Go 的 runtime buffer,再拷进 socket 内核缓冲区——典型四次拷贝中的两次 CPU 参与。
-
http.ServeFile和http.ServeContent内部用的全是io.Copy,不是file.WriteTo,连触发零拷贝的入口都没有 - 哪怕你传入一个打开的
*os.File,只要目标是http.ResponseWriter或tls.Conn,就自动降级为read+write - HTTP/2、chunked 编码、gzip、任何中间件(如日志、压缩)都会切断零拷贝路径
怎样让 *os.File 真正走 sendfile
唯一可靠路径是显式调用 file.WriteTo(conn),且确保 conn 是未包装的 *net.TCPConn。常见错误是误以为 w.(http.Hijacker).Hijack() 拿到的就是可用连接——它返回的 net.Conn 虽然裸,但可能已被标记为“已关闭”或处于非阻塞状态,需进一步校验 fd 有效性。
- 源文件必须用
os.O_RDONLY打开,不能是/proc、管道、设备文件 - 目标连接必须是
*net.TCPConn类型,不能是bufio.Writer或tls.Conn包裹过的实例 - 验证是否生效:用
strace -e trace=sendfile,read,write ./your-server启动,只看到sendfile调用才算成功 - 容器环境(Docker/K8s)中,因挂载命名空间隔离,
sendfile可能被内核静默降级,strace也看不到报错
代理场景下只能靠 syscall.Splice 拼路径
当你需要把上游 HTTP 响应体(比如 resp.Body)直接转发给下游客户端时,file.WriteTo 失效——因为 resp.Body 是 io.Reader,不是 *os.File。此时唯一可行的零拷贝路径是 syscall.Splice,但它对 fd 类型极其挑剔:至少一端必须是 pipe。
- 先用
syscall.Pipe2创建一对 pipe fd - 用
syscall.Splice把上游 conn 的读端 →pipe[1],再把pipe[0]→ 下游 conn 的写端 - 每次
Splice调用前必须处理EINTR(中断重试)和EAGAIN(非阻塞等待) -
Splice不感知 goroutine 抢占,若网络拥塞或磁盘慢,整个 M 线程会被锁死,拖垮调度器
真正落地零拷贝,从来不是加个 flag 或换函数名就能成的事——fd 管理、错误重试、协议封装、超时控制、容器兼容性,全得自己补全。漏掉任意一环,性能就掉回 read+write 原点。











