go标准库无sendfile函数,需用syscall或x/sys/unix调用;linux可用零拷贝,macos/windows不支持;推荐用net/http.servecontent替代手动调用,它自动适配系统并支持http标准特性。

Go 里 Sendfile 不是标准库函数,别白找
Go 标准库没有叫 Sendfile 的函数——这是 C 系统调用名,Linux 下对应 sendfile(2),Go 只在底层(如 net/http 的文件服务)悄悄用了它,但不暴露给用户直接调用。
想手动触发零拷贝发送大文件,得走 syscall.Sendfile 或封装好的第三方包(比如 golang.org/x/sys/unix),而且只在 Linux 生效;macOS 是 sendfile,Windows 完全不支持。
-
syscall.Sendfile参数顺序和语义容易写反:目标 fd 在前、源 fd 在后,传错会直接EINVAL - 源 fd 必须是普通文件(
os.File),不能是 pipe、socket 或 bytes.Buffer - 目标 fd 必须是 socket(且是 TCP 连接的 fd),不能是普通文件或 stdout
- 如果源文件被 mmap 或并发 truncate,
Sendfile可能读到脏数据或 panic
用 net/http.ServeContent 替代手写 Sendfile 更稳妥
大多数 Web 场景下,你真正需要的不是裸调 Sendfile,而是高效、带 range、etag、last-modified 的大文件响应——net/http.ServeContent 就干这事,它内部在 Linux 上自动降级用 sendfile,其他系统用常规 read/write 循环,行为一致还省心。
关键点是:必须显式设置 Content-Length 和 Last-Modified,否则 ServeContent 会退化为 chunked 编码,失去零拷贝机会。
立即学习“go语言免费学习笔记(深入)”;
- 用
http.ServeFile不够灵活,不支持自定义 header 或条件响应;ServeContent才是正解 - 传入的
io.ReadSeeker必须支持Stat()(比如*os.File),否则 fallback 到内存读取 - 别自己调
WriteHeader再调ServeContent,它会自己处理状态码;重复写 header 会导致http: multiple response.WriteHeader calls
http.ServeContent(w, r, filename, modTime, file)
手动调 unix.Sendfile 的最小可行路径
真要绕过 HTTP 栈、在 TCP 连接上直发大文件(比如做私有协议文件传输),用 golang.org/x/sys/unix 是目前最干净的方式,比直接用 syscall 更跨平台、文档更全。
注意:fd 必须是原始整数,不能用 file.Fd() 后直接传——要确保文件没被 close,且 socket 处于可写状态(比如已三次握手完成)。
- 先
file.Seek(0, 0),再unix.Sendfile,否则可能从中间开始发 - 返回值是实际发送字节数,可能小于文件大小(比如网络阻塞),需循环调用并检查
errno == unix.EAGAIN - Windows 下这个调用直接 panic,必须加
// +build linux构建约束 - Go 1.21+ 中
unix.Sendfile第四个参数是 offset 指针,传&offset,不是值;传错会 segfault
性能差异没想象中大,别过早优化
实测 100MB 文件在千兆内网下:Sendfile 比 io.Copy 快 10%~15%,但 CPU 使用率低 30%+;一旦加了 TLS,这个差距基本消失——因为加密/解密成了瓶颈,零拷贝省下的内存拷贝时间被掩盖了。
更现实的瓶颈往往是磁盘 I/O(尤其是机械盘)、TCP 窗口大小、客户端接收缓冲区,而不是 Go 的 copy loop。
- 用
io.CopyBuffer(w, r, make([]byte, 1 配 1MB buffer,性能已接近 <code>Sendfile,且完全跨平台 - HTTP/2 或 QUIC 场景下,
Sendfile无法生效,协议栈本身就不走传统 socket write - 容器环境里,如果挂载的是 NFS 或 overlayfs,
Sendfile可能静默退化为普通读写,连 error 都不报











