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:
| Type | What it reads from |
|---|---|
*os.File | disk files |
http.Request.Body | incoming HTTP request bodies |
net.Conn | network connections (TCP, UDP) |
*bytes.Buffer | in-memory byte buffers |
*strings.Reader | in-memory strings |
gzip.Reader | compressed 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:
- Process
nbytes, notlen(buf)โ the slice may only be partially filled. - Check
errafter processingnbytes โio.EOFcan arrive on the same call that returns the final bytes, so discardingnbefore checkingerrloses 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?