Zero-Copy I/O (sendfile, splice, Copy)
Zero-Copy I/O (sendfile, splice, Copy)
Overview
Moving bytes through user space burns CPU. Linux offers sendfile and splice to reduce copies. Go’s io.Copy tries optimized paths when concrete types implement ReaderFrom / WriterTo (and OS support exists).
Popular writeups (widely shared among systems Go engineers) stress: TLS and some wrappers disable the fast path.
Diagram: Copy fast path
io.Copy(dst, src)
│
├── dst has ReadFrom? ──yes──► optimized path
│
├── src has WriteTo? ──yes──► optimized path
│
└── else ──► buffered copy loop
TLS / custom wrappers often force the slow path
Copy Dispatch
io.Copy(dst, src)
-> if dst has ReaderFrom: dst.ReadFrom(src)
-> else if src has WriterTo: src.WriteTo(dst)
-> else generic buffer loop
*os.File and *net.TCPConn participate in platform-specific fast paths when combinations allow.
sendfile / splice (Linux)
Typical wins:
- File → socket static file serving
- Socket → socket proxying (sometimes via splice)
Usually not available end-to-end when:
- TLS wraps the connection (encryption needs bytes in user space)
- Custom
io.Readerwrappers break type assertions - Non-Linux platforms fall back to buffered copy
Practical Patterns
// Prefer typed paths
if _, err := io.Copy(dstFile, srcFile); err != nil { ... }
// Cap memory even on slow path
lr := io.LimitReader(src, max)
_, err = io.Copy(dst, lr)// HTTP: FileServer / ServeContent already try efficient paths
http.ServeContent(w, r, name, modtime, content)Measuring
perf stat -e cycles,instructions,cache-misses ./proxy # Linux
go test -bench=Copy -benchmemCompare:
io.Copyfile→file- Manual
Read/Write32KiB buffer - File→TLS conn (expect less zero-copy)
Experiment
go mod init example
go test -bench=. -benchmempackage zc_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
)
func BenchmarkCopyFile(b *testing.B) {
dir := b.TempDir()
src := filepath.Join(dir, "s")
dst := filepath.Join(dir, "d")
data := bytes.Repeat([]byte("x"), 1<<20)
if err := os.WriteFile(src, data, 0o644); err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(data)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
in, _ := os.Open(src)
out, _ := os.Create(dst)
_, _ = io.Copy(out, in)
in.Close()
out.Close()
}
}What to notice: Kernel paths dominate large copies; wrappers and TLS change the story more than micro-tuning buffer sizes alone.
Try next: Wrap the reader in a custom type without WriterTo and observe any bench regression.