io

Guided tour · I/O & Files · pkg.go.dev →

The Reader/Writer interfaces every streaming API is built on — and the helpers to copy, limit, tee, and combine them.

Stream-shaped I/O — Reader / Writer interfaces and the helpers that compose them. Most of the rest of the stdlib is built on these.

Read everything from a Reader
b, err := io.ReadAll(r)
Pipe a Reader to a Writer
_, err := io.Copy(dst, src)
Throw bytes away
io.Copy(io.Discard, r)
Cap at N bytes
r = io.LimitReader(r, 1<<20)
Tee (read + write through)
r = io.TeeReader(r, audit)
Chain readers
r := io.MultiReader(header, body)
Multiple writers at once
w := io.MultiWriter(file, os.Stdout)

The core interfaces

Reader and Writer are arguably the most important interfaces in Go. Everything streamable implements them.

Reader: Read(p []byte) (n int, err error)

Returns how many bytes were filled into p. May return n > 0 AND err = io.EOF on the last read — always handle bytes before checking err.

r := strings.NewReader("hello")
buf := make([]byte, 3)
for {
    n, err := r.Read(buf)
    if n > 0 {
        fmt.Printf("got %d: %q\n", n, buf[:n])
    }
    if err == io.EOF {
        break
    }
}
Output
got 3: "hel"
got 2: "lo"

Writer: Write(p []byte) (n int, err error)

var b bytes.Buffer  // *bytes.Buffer implements io.Writer
fmt.Fprint(&b, "hi")
b.Write([]byte(" there"))
fmt.Println(b.String())
Output
hi there

Copy, CopyN, CopyBuffer

The bread-and-butter of streaming: move bytes from a Reader to a Writer.

io.Copy — copy until EOF

Uses WriterTo / ReaderFrom fast paths when available (e.g., *os.File). Returns bytes copied.

r := strings.NewReader("hello world")
n, _ := io.Copy(os.Stdout, r)
fmt.Printf("\n(%d bytes)\n", n)
Output
hello world
(11 bytes)

io.CopyN — copy exactly N bytes

r := strings.NewReader("hello world")
io.CopyN(os.Stdout, r, 5)
Output
hello

io.CopyBuffer — bring your own buffer

Avoids allocating a 32 KiB temporary buffer per call. Use when Copy is in a hot path.

buf := make([]byte, 4096)
io.CopyBuffer(dst, src, buf)

Read helpers

io.ReadAll — read everything into memory

Convenient but only safe for bounded inputs. For untrusted sources, wrap in io.LimitReader.

b, _ := io.ReadAll(strings.NewReader("abc"))
fmt.Println(string(b))
Output
abc

io.ReadFull — fill a buffer exactly or error

r := strings.NewReader("hello")
buf := make([]byte, 5)
_, err := io.ReadFull(r, buf)
fmt.Println(string(buf), err)
Output
hello <nil>

io.LimitReader — cap the bytes you'll read

Critical for accepting untrusted input: stops at N bytes even if the source has more.

r := io.LimitReader(strings.NewReader("abcdefg"), 3)
b, _ := io.ReadAll(r)
fmt.Println(string(b))
Output
abc

Combining and adapting streams

io.MultiReader — concat Readers

r := io.MultiReader(
    strings.NewReader("hello "),
    strings.NewReader("world"),
)
io.Copy(os.Stdout, r)
Output
hello world

io.MultiWriter — tee to many Writers

var buf bytes.Buffer
w := io.MultiWriter(os.Stdout, &buf)
fmt.Fprintln(w, "logged")
fmt.Println("captured:", buf.String())
Output
logged
captured: logged

io.TeeReader — observe a stream as it's read

Every byte read from the returned Reader is also written to the given Writer. Great for hashing or logging as you stream.

var captured bytes.Buffer
src := strings.NewReader("hello")
r := io.TeeReader(src, &captured)
io.Copy(io.Discard, r)
fmt.Println(captured.String())
Output
hello

io.Pipe — in-memory synchronous pipe

Write in one goroutine, Read in another. Useful when an API wants a Reader but you produce bytes procedurally.

pr, pw := io.Pipe()
go func() {
    defer pw.Close()
    fmt.Fprint(pw, "hello")
}()
io.Copy(os.Stdout, pr)
Output
hello

Sinks and sentinels

io.Discard — /dev/null for Writer

Use when you need to read a whole stream but don't care about the bytes (e.g., draining an HTTP body before Close).

io.Copy(io.Discard, someReader)

io.EOF

The sentinel error Readers return when there's no more data. Not a real error — just end of stream. Compare with ==.