Go标准库不支持sendfile,需手动调用syscall或x/sys/unix;必须配合原始socket fd、非阻塞模式、偏移控制及HTTP头对齐,否则易出错;多数场景下io.Copy等标准方案更安全高效。

Go 里 sendfile 系统调用到底能不能用?
不能直接用——标准库 net.Conn 没暴露 sendfile,os.File.Read + conn.Write 是纯用户态拷贝,不走零拷贝路径。
Go 运行时封装了底层系统调用,但对 sendfile(Linux)、copyfile(macOS)或 TransmitFile(Windows)这类内核态文件到 socket 的直通机制,没有提供安全、跨平台的封装接口。想用,得绕过标准库,自己调系统调用。
-
sendfile要求源 fd 是普通文件(S_ISREG),不能是 pipe、socket 或 /proc 下的伪文件 - 目标 fd 必须是 socket,且协议栈支持(TCP/UDP 均可,但 UDP 有 size 限制)
- Linux 上要求内核 ≥2.6.33 才支持非阻塞 socket 配合
sendfile;老内核会阻塞整个 goroutine - Go 的
runtime.entersyscall不感知sendfile是否可中断,一旦陷入,无法被抢占,可能拖慢调度器
怎么在 Go 中真正调用 sendfile?
靠 syscall.Syscall6 或更安全的 golang.org/x/sys/unix 包,手动拼系统调用参数。别手写汇编,也别用 cgo 封装——没必要,且破坏交叉编译能力。
关键点不是“能不能调”,而是“调完怎么和 net.Conn 协同”。你不能把 sendfile 和 conn.Write 混用:前者绕过 conn 的 write buffer 和 deadline 控制,后者不知道数据已发。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
unix.Sendfile(Linux)或unix.CopyFileRange(5.3+ 内核),传入原始 socket fd(从conn.(*net.TCPConn).SyscallConn()获取) - 调用前需
syscall.SetNonblock(fd, true),否则阻塞;但非阻塞模式下sendfile可能返回EAGAIN,要重试 - 务必检查返回值:成功时返回实际发送字节数;失败时看
errno,EINTR可重试,EAGAIN需等EPOLLOUT事件 - 别忘了设置 socket 的
SO_SNDTIMEO,sendfile不受SetWriteDeadline影响
fd, _ := conn.(*net.TCPConn).SyscallConn()
fd.Control(func(fd uintptr) {
unix.Sendfile(int(fd), int(file.Fd()), &offset, count)
})
sendfile 在 HTTP 文件服务中为什么常被误用?
很多人以为用 sendfile 就能加速静态文件服务,结果反而变慢或出错——问题不在技术本身,而在上下文没对齐。
HTTP/1.1 分块传输、范围请求(Range)、gzip 压缩、ETag 校验、缓存头注入……这些都发生在应用层。一旦跳过 http.ResponseWriter 直接怼 socket,你就得自己处理所有 HTTP 协议细节,包括状态行、header 分隔、CRLF、chunked 编码、连接复用逻辑。
- 用
sendfile前必须确认:请求是完整 GET、无 Range、无 Accept-Encoding: gzip、Connection: close(或你自己管理 keep-alive) - HTTP header 已经写入 socket?那
sendfile必须从 header 后偏移处开始,否则文件内容混进 header 里 - 文件 size > 2GB?32 位 offset 在
sendfile中会截断,要用copy_file_range或分段调用 - 容器环境(如 Docker)挂载的 overlayfs 或 fuse 文件系统,可能不支持
sendfile,返回EINVAL
替代方案比硬上 sendfile 更实用
除非压测明确卡在 memcpy(比如万兆网卡跑满、CPU sys% > 60%),否则优先用标准库的 io.Copy + bufio.Writer + http.ServeContent。
Go 1.16+ 的 io.CopyN 和 net.Buffers 已优化大块写入;配合 SetWriteBuffer 调大 socket 发送缓冲区,实测吞吐差距不到 8%。而自己维护 sendfile 逻辑,要处理信号、fd 生命周期、goroutine 阻塞、错误恢复、测试覆盖——成本远高于收益。
- 小文件(http.ServeFile,它内部用
io.Copy,足够快 - 大文件 + 高并发:用
http.ServeContent,自动处理 Range、If-Modified-Since,header 安全 - 真要零拷贝:考虑用户态协议栈(如
dpdk-go)或 eBPF 辅助,而不是在 socket 层抠sendfile - 调试时注意:
strace -e trace=sendfile,write,read才能看到真实路径,netstat看不到零拷贝发生
真正难的不是调哪个系统调用,是怎么让零拷贝行为和 Go 的 runtime 抢占、网络 poller、error handling 三者不打架。很多线上事故,都是因为 sendfile 返回 EAGAIN 后没等事件就 panic 了。










