Example: io.MultiReader
io.MultiReader combines multiple io.Reader values into a single sequential input stream. It allows callers to consume several data sources in order without manually switching between Readers.
It solves the problem of sequential stream composition.
For example, an application can combine:
- An in-memory protocol header + a file payload
- Metadata + a large file
- Multiple file segments
- Multiple in-memory buffers
- A dynamically generated prefix + a network stream
into a single io.Reader.
The core value of io.MultiReader is:
It composes the order in which data is read, not the data itself.
The core trap is:
io.MultiReaderonly manages sequential reading. It does not manage concurrency, error recovery, or the lifecycle of underlying resources.
Quick Start
The simplest scenario: combine two Readers into one sequential Reader.
Expected output:
The caller sees one continuous input stream:
But the data is actually provided by two independent Readers in sequence.
Core Contract
io.MultiReader(readers...) returns an io.Reader that consumes the supplied Readers sequentially.
Conceptually:
For the caller, multiple independent Readers appear as one continuous input stream.
If no Readers are provided, io.MultiReader() returns an empty Reader that always returns io.EOF.
If an input Reader immediately returns io.EOF, MultiReader skips it and continues with the next Reader without exposing an intermediate EOF to the caller.
Success
When MultiReader returns err == nil, it only means that the current Read operation successfully returned some data.
It does not mean that:
- The current Reader has been exhausted.
- All Readers have been consumed.
- The combined stream has reached EOF.
Only after all underlying Readers have returned io.EOF will MultiReader return the final io.EOF to the caller.
An intermediate io.EOF is consumed internally and treated as the boundary for moving to the next Reader.
Failure and Boundary Behavior
The core rule is:
io.MultiReadertreats onlyio.EOFas a normal boundary between Readers. Other errors are propagated.
It does not:
- Retry a failed Reader.
- Skip a failed Reader.
- Continue with later Readers after a non-EOF error.
- Read multiple Readers concurrently.
- Close underlying resources.
When to Use
Sequentially Combine Multiple Data Sources
Use io.MultiReader when several data sources logically form one continuous sequential input stream.
Typical scenarios include:
- In-memory header + file payload
- Metadata + large file
- Multiple file segments
- Dynamically generated prefix + network stream
- Multiple independent buffers
Why: io.MultiReader combines multiple data sources without constructing a new complete []byte. The resulting stream remains fully incremental.
For example:
The caller only needs to work with one io.Reader and does not need to manually switch between the underlying data sources.
When Not to Use
When Multiple Sources Must Be Read Concurrently
io.MultiReader strictly consumes Readers in sequence.
If the first Reader blocks, later Readers are not accessed:
It is therefore not suitable when multiple independent data sources need to make progress concurrently.
Risk: If an earlier network connection or other Reader blocks for a long time, the entire combined stream is blocked and later Readers cannot make progress.
Use Instead: Depending on the application, use goroutines, channels, io.Pipe, or an application-level scheduling mechanism to explicitly control concurrency and cancellation.
The key distinction is:
io.MultiReaderis a sequential composition primitive, not a concurrency primitive.
Quick Reference
Question: Does io.MultiReader read multiple Readers concurrently?
Answer: No. It consumes Readers strictly in the order provided. The next Reader is accessed only after the current Reader returns io.EOF.
Question: If an intermediate Reader returns an error, will io.MultiReader continue with the next Reader?
Answer: No. io.EOF is treated as a normal boundary between Readers. Other errors are returned immediately, and subsequent Readers are not read.
Question: Does io.MultiReader automatically close underlying io.ReadCloser values?
Answer: No. io.MultiReader itself implements only io.Reader. It does not wrap or proxy the Close methods of underlying resources.
Key Point: io.MultiReader provides sequential stream composition, not concurrent scheduling, error recovery, or resource management.
Production Scenario
Streaming a Generated Header Before a Large File
Problem: An application needs to send a dynamically generated header followed by a large file.
If the header and the entire file are first combined into a single []byte:
memory usage grows with the size of the file.
io.MultiReader allows the application to combine the two data sources while preserving streaming behavior:
Key Engineering Points:
- The header remains in memory, while the file content is read incrementally.
io.MultiReaderdoes not load the entire file into memory.- The downstream consumer only needs to handle one
io.Reader; it does not need to manually switch between data sources. io.MultiReaderdoes not closefile; the caller remains responsible forfile.Close().- If a non-EOF error occurs while reading the file, the error is returned through the combined Reader.
- In a real production scenario, the combined Reader can be passed directly to an HTTP request, network connection, or another Writer without first calling
io.ReadAll. io.Copyprocesses the stream incrementally using its own internal buffer. That buffer belongs toio.Copyand is not memory accumulated byio.MultiReader.
Common Failure Modes
Failure: Assuming MultiReader Closes Underlying Resources
❌ Risky Code
Why It Fails
io.MultiReader itself implements only io.Reader. It does not wrap or proxy the Close methods of underlying io.ReadCloser values, so the combined Reader cannot be used to close those resources.
Reading the combined stream to EOF does not automatically close resp.Body.
If the underlying Reader is an HTTP response body, file, network connection, or another closeable resource, the caller that owns the resource remains responsible for closing it.
Otherwise, the application may encounter:
- HTTP connections that cannot be reused efficiently.
- Leaked file descriptors.
- Network connections that remain open longer than necessary.
- Other underlying resources that are not released promptly.
Therefore:
io.MultiReadercomposes reading behavior, not resource ownership.
✅ Correct Approach
The ownership of resp.Body remains with the caller that received or created it.
If the lifetimes of multiple underlying resources genuinely need to be bound to the lifetime of the combined stream, the application can define a small io.ReadCloser wrapper that explicitly owns and closes those resources.
For example:
This approach is useful when resource cleanup is intentionally part of the combined stream's ownership contract.
However, this is an application-level resource-management strategy, not a capability provided by io.MultiReader itself.
Failure: Assuming EOF Means Every Reader Was Successfully Consumed
Consider:
If firstReader returns a non-EOF error:
MultiReader does not skip the failed Reader or continue with secondReader.
Therefore, it is incorrect to assume:
"MultiReader eventually consumes all Readers."
The correct model is:
Only when the preceding Reader reaches
io.EOFcan the next Reader be accessed.
If the application requires retries, fallback Readers, or skipping failed data sources, that behavior must be implemented explicitly outside io.MultiReader.
API Boundary Comparison
Key Takeaways
io.MultiReader is a small but useful stream-composition primitive.
Its core contract is simple:
Treat multiple Readers as one sequential Reader.
The important production rules are:
- Readers are consumed strictly in the order provided.
- The next Reader is accessed only after the current Reader returns
io.EOF. - An intermediate
io.EOFis treated as a transition to the next Reader. - A non-EOF error stops further progress and is returned to the caller.
- It does not read multiple sources concurrently.
- It does not aggregate all input into a single in-memory buffer.
- It does not manage the lifetime of underlying resources.
- If resource ownership must be bundled, an application-level
io.ReadCloserwrapper can be introduced explicitly.
The most important engineering distinction is:
io.MultiReaderonly composes the sequential flow of data. It does not manage concurrency, error recovery, or underlying resource lifetimes.
Use it when multiple data sources logically form one sequential stream.
Do not use it as a concurrency primitive, resource manager, or retry mechanism.