๐Ÿ“–io.ReaderLESSON

io.Reader

The Most Fundamental Interface in Go

io.Reader is the cornerstone of Go's I/O model. Its definition is deceptively simple:

Read fills the byte slice p with up to len(p) bytes and returns how many bytes were actually read (n). When there is no more data, it returns 0, io.EOF.

If you are coming from Python, think of io.Reader as the Go equivalent of a file object's read() method โ€” except it works on any data source, not just files.

Why It Matters

Every type in Go's standard library that produces bytes implements io.Reader:

TypeWhat it reads from
*os.Filedisk files
http.Request.Bodyincoming HTTP request bodies
net.Connnetwork connections (TCP, UDP)
*bytes.Bufferin-memory byte buffers
*strings.Readerin-memory strings
gzip.Readercompressed streams

A function that accepts io.Reader works with all of these without any changes. This is Go's approach to polymorphism through interfaces โ€” write once, works everywhere bytes flow.

strings.NewReader โ€” Creating a Reader for Testing

The easiest way to get an io.Reader without touching the filesystem is strings.NewReader. It wraps a string as an io.Reader so you can pass it to any function that expects one:

Output:

The Read call fills the buffer as many times as needed, returning fewer bytes on the last chunk. This chunked behavior is fundamental โ€” do not assume a single Read returns everything.

Reading in Chunks

The Read contract has two rules you must always follow:

  1. Process n bytes, not len(buf) โ€” the slice may only be partially filled.
  2. Check err after processing n bytes โ€” io.EOF can arrive on the same call that returns the final bytes, so discarding n before checking err loses data.

In Python you would write data = f.read(512) and check if not data. The Go pattern is more explicit because the same call that delivers the last bytes also delivers the io.EOF signal.

io.ReadAll โ€” Reading Everything at Once

When the entire content fits comfortably in memory, io.ReadAll is the convenient shortcut:

io.ReadAll is the right tool for small, bounded inputs (configuration files, test fixtures, API responses under a known size limit). Avoid it for unbounded streams โ€” a misbehaving client could send gigabytes and exhaust memory.

bufio.Scanner โ€” Line-by-Line Reading

When input is text and you want one line at a time, bufio.Scanner is cleaner than manual chunk reads:

Output:

scanner.Scan() advances to the next token (line by default) and returns false at EOF. Always check scanner.Err() after the loop โ€” a false return from Scan could mean either clean EOF or an I/O error. This mirrors Python's for line in f: idiom but makes errors explicit.

io.Reader Composition โ€” The Real Power

Because io.Reader is an interface, it is trivially composable. The standard library ships several adapters that wrap one reader to produce a new one:

io.LimitReader

Caps how many bytes can be read โ€” essential for safely handling untrusted input:

io.TeeReader

Reads from one reader and simultaneously writes everything to a writer โ€” useful for logging, checksumming, or caching while streaming:

Output:

These adapters work because they all accept and return io.Reader. You can stack them freely:

No inheritance, no framework โ€” just interface composition. This is the design philosophy that makes Go's standard library feel small yet cover everything.

Knowledge Check

What does the Read method return when it has reached the end of the data?

Which statement best describes what strings.NewReader returns?

Why should a function accept io.Reader instead of *os.File when it only needs to read bytes?