Zero-Copy I/O (sendfile, splice, Copy)

Updated

September 8, 2026

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.Reader wrappers 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 -benchmem

Compare:

  1. io.Copy file→file
  2. Manual Read/Write 32KiB buffer
  3. File→TLS conn (expect less zero-copy)

Experiment

go mod init example
go test -bench=. -benchmem
package 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.