Why does io.Reader's Read method take a caller-supplied []byte instead of returning the bytes it read?
answer
- who allocates the memory?
- one slice, many calls
- a stream can be gigabytes
- the caller sets the memory budget
basics
~20 sio.Reader.Read writes into a byte slice the caller supplies, so the caller decides how much memory a stream costs and can reuse one slice for every call. Returning a fresh slice per read would allocate on every call.
solid answer
~50 sThe method is `Read(p []byte) (n int, err error)`. The caller allocates `p` once, and every call fills up to `len(p)` bytes into it and reports how many it actually wrote in `n`. That lets you stream a file of any size through a fixed amount of memory: allocate a 32 KiB slice, loop, and process `n` bytes each time. If `Read` returned its own slice, every call would allocate, and the garbage collector would see one slice per read for the whole stream. It also keeps the interface to a single method, so anything that can produce bytes -- a file, a network connection, an in-memory `bytes.Reader`, a decompressor -- satisfies it without importing anything. `Read` is allowed to write fewer than `len(p)` bytes, and it reports the end of the stream by returning `io.EOF`, which is a normal ending rather than a failure.
code
go · 7 linestype Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}go deeper
Be ready to write the signature Read(p []byte) (n int, err error) from memory and say what n means. Show the read loop, use only the first n bytes, and stop on io.EOF without treating it as a failure.
An interviewer at this level expects you to explain the allocation argument: the caller owns the slice, reuses it, and therefore bounds memory independently of stream size. Mention that a short count is legal mid-stream.
Show the judgment behind buffer sizing under concurrency -- one slice per in-flight stream, so a large buffer multiplied by thousands of connections is a memory decision, not a micro-optimisation.
Own the argument for why capability interfaces stay one method and are composed by embedding. That choice is why unrelated packages interoperate, and it is the model to follow in your own exported APIs.
## The declaration The `io` package defines two interfaces that most of Go's I/O is built on, and each has exactly one method: ```go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } ``` `Read` is given a byte slice `p` that the **caller** allocated. It writes bytes into that slice, up to `len(p)` of them, and returns `n`, the count it actually wrote, plus an error. The bytes live in the caller's memory; the reader never hands ownership of anything back. ## Why the caller supplies the memory Imagine the alternative signature, `Read() ([]byte, error)`. To satisfy it, every implementation would have to allocate a slice on every single call, because it cannot know what the caller will do with the previous one. Copying a 4 GB file at 32 KiB per read is about 130,000 calls; that shape would produce 130,000 heap allocations that the garbage collector must trace and free, for data that is consumed and forgotten immediately. With the real signature the caller allocates once and reuses. That is exactly what `io.Copy` does internally: it makes one scratch slice (32 KiB in the current implementation) and drives the whole transfer through it, no matter how large the stream is. Memory use is bounded by a decision the *caller* made, not by the size of the data, which is the whole point of streaming. The caller also gets to choose the trade-off. A tiny slice means more system calls and more loop iterations; a large one means fewer calls but more resident memory per concurrent stream. A server handling ten thousand connections cares about that number; a one-shot command-line tool does not. Only the caller knows which situation it is in. ## What the return values mean - `n` is the number of bytes this call placed into `p`, and `0 <= n <= len(p)`. Only `p[:n]` is meaningful; the rest of the slice still holds whatever was there before. - `Read` may return a short count even when the stream is far from finished. A network read returns what has arrived; a pipe returns what is in the pipe. Code that assumes `n == len(p)` is wrong. - The end of the stream is reported as the error `io.EOF`. That is a sentinel value meaning "no more data", not a malfunction -- treating it as a failure and logging it is a classic beginner mistake. Because of these rules, consuming a reader is always a loop rather than a single call. You read, you use the `n` bytes you got, and you keep going until the error tells you to stop. ## Why one method Go has no `implements` keyword, so any type with a method of that exact signature is an `io.Reader`. Keeping the interface to a single method makes it trivially cheap to satisfy: `*os.File`, `net.Conn`, `*bytes.Buffer`, `*strings.Reader`, an HTTP request body and a decompressing wrapper are all readers, and none of them coordinated with each other to be so. Every function in the standard library that takes an `io.Reader` therefore works with all of them, including test doubles you write yourself in three lines. Capabilities beyond "give me bytes" live in their own single-method interfaces -- `io.Closer` is `Close() error`, `io.Seeker` is `Seek(offset int64, whence int) (int64, error)` -- and are combined by embedding when a type genuinely offers more: `io.ReadCloser`, `io.ReadWriter`, `io.ReadWriteCloser`. That is why the small contract matters. If `Read` had been bundled into a fat interface with seeking and closing, a network connection could not implement it, and none of this composition would exist. ## The other half of the bargain Because the slice belongs to the caller and is normally reused, the `io` documentation puts a duty on implementations: they must not retain `p` after the call returns, and a `Write` implementation must not modify the caller's data even temporarily. The caller is entitled to overwrite `p` the instant `Read` or `Write` returns. That rule is what makes buffer reuse safe, and it is the price of not allocating. ## A rule of thumb When you write code that consumes bytes, take an `io.Reader` rather than a concrete type, allocate one buffer outside the loop, and always look at `n` before you look at the slice. When you implement one, fill as much of `p` as you cheaply can, return the honest count, and return `io.EOF` once there is nothing left.
- What does a Read implementation return once the stream has no more data?It returns the error `io.EOF`. That is the normal, expected end of a stream, not a malfunction: code checks for it explicitly and finishes successfully. Wrapping it in a "read failed" error, or logging it as a problem, is a misreading of the contract. Any other non-nil error is a real failure.
- Why isn't Close part of the io.Reader interface?Because plenty of readers have nothing to close -- `strings.Reader`, `bytes.Buffer`, a decoder over an in-memory slice. Go keeps each capability in its own one-method interface and combines them by embedding, so a type that really does own a resource implements `io.ReadCloser`, and everything else stays satisfiable in one method.
- How would you write an in-memory io.Reader for a test?You rarely need to hand-write one: `strings.NewReader("...")` and `bytes.NewReader(b)` are readers over in-memory data, so any function that accepts an `io.Reader` can be tested with no files or sockets involved. That interchangeability is the practical payoff of the one-method contract.
It is like handing a waiter your own container instead of getting a new takeaway box each time: you decide how big it is, and you can bring the same one back all evening.
saying these in an interview costs you the question
- Thinks Read allocates and returns a fresh slice each call
- Assumes Read always fills the entire slice you pass it
- Treats io.EOF as an error to log and abort on
- Allocates a new buffer inside every loop iteration
- Reads the whole slice instead of only the first n bytes