io and bufio Composition
io and bufio Composition
Overview
The io package is Go’s streaming contract. Small interfaces (Reader, Writer, Closer) let files, sockets, compressors, and crypto share the same pipelines.
Deep patterns also appear in I/O and Streaming. This chapter is the stdlib composition cheatsheet.
Core contracts
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}- Process
n > 0before checkingerr(includingio.EOF). io.EOFmeans end of stream, not a hard failure forReadloops.- Prefer
io.Copyover manual loops — it usesWriterTo/ReaderFromwhen available.
Helpers worth memorizing
| Helper | Purpose |
|---|---|
io.Copy / CopyBuffer |
Stream A → B efficiently |
io.ReadAll |
Entire stream to memory (cap it!) |
io.LimitReader |
Hard cap bytes read |
io.TeeReader |
Read + fork to a Writer (checksums) |
io.MultiReader |
Concatenate readers |
io.MultiWriter |
Fan-out writes |
io.Pipe |
In-process streaming between goroutines |
io.NopCloser |
Wrap Reader as ReadCloser |
io.Discard |
Drain without storing |
const max = 1 << 20 // 1 MiB
data, err := io.ReadAll(io.LimitReader(r, max+1))
if err != nil {
return err
}
if len(data) > max {
return fmt.Errorf("payload exceeds %d bytes", max)
}bufio: amortize syscalls
br := bufio.NewReader(f)
line, err := br.ReadString('\n')
bw := bufio.NewWriter(f)
fmt.Fprintln(bw, "hello")
if err := bw.Flush(); err != nil {
return err
}| Type | Role |
|---|---|
bufio.Reader |
Peek, ReadBytes, ReadString |
bufio.Writer |
Buffered writes; always Flush |
bufio.Scanner |
Tokenize lines/words (watch token size) |
sc := bufio.NewScanner(f)
// optional: sc.Buffer(buf, 1024*1024) for long lines
for sc.Scan() {
line := sc.Text()
_ = line
}
return sc.Err()Default Scanner max token is 64KiB — raise it for pathological log lines or reject them.
Composition examples
Checksum while uploading
h := sha256.New()
tr := io.TeeReader(src, h)
if _, err := io.Copy(dst, tr); err != nil {
return err
}
sum := h.Sum(nil)Bound + discard remainder
// Read at most n bytes; ensure caller can still Close the body
lr := io.LimitReader(resp.Body, n)
_, err := io.Copy(dst, lr)Pitfalls
- Forgetting
Closeonresp.Body/ files → connection or FD leaks. - Unbounded
ReadAllon user input → memory exhaustion. - Nested
bufio.Readerwithout understanding buffered leftovers. - Ignoring short writes from custom
Writers.
Runnable example
go mod init example
go run .package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"strings"
)
func main() {
src := strings.NewReader("hello stream\nsecond line\n")
// LimitReader + ReadAll
limited, _ := io.ReadAll(io.LimitReader(src, 5))
fmt.Printf("limited=%q\n", limited)
// reset-like: new reader
src = strings.NewReader("hello stream\nsecond line\n")
h := sha256.New()
var out bytes.Buffer
tr := io.TeeReader(src, h)
if _, err := io.Copy(&out, tr); err != nil {
panic(err)
}
fmt.Printf("copied=%q\n", out.Bytes())
fmt.Println("sha256", hex.EncodeToString(h.Sum(nil))[:16]+"...")
// Scanner lines
sc := bufio.NewScanner(strings.NewReader("a\nb\nc\n"))
for sc.Scan() {
fmt.Println("line:", sc.Text())
}
fmt.Println("scan err:", sc.Err())
// MultiWriter
var a, b bytes.Buffer
mw := io.MultiWriter(&a, &b)
io.WriteString(mw, "x")
fmt.Printf("a=%q b=%q\n", a.String(), b.String())
}Expected output:
limited="hello"
copied="hello stream\nsecond line\n"
sha256 f74a77476883f531...
line: a
line: b
line: c
scan err: <nil>
a="x" b="x"
What to notice: - LimitReader stops early without error when the cap is hit mid-stream. - TeeReader is the clean way to hash or log while still delivering bytes. - Scanner is for tokens; io.Copy is for bulk.
Try next: Use io.Pipe with two goroutines: one writes lines, one counts them with bufio.Scanner.