skip to content

In Go's io package, what do io.MultiReader and io.MultiWriter each produce?

level: juniorimportance: should knowfreq 48%

answer

  1. one joins, the other duplicates
  2. sources in sequence, destinations in one pass
  3. no goroutines, no closing, no buffering
  4. the fan-out stops at the first bad destination

basics

~10 s

io.MultiReader returns a Reader that reads its sources one after another and reports EOF only after the last one ends. io.MultiWriter returns a Writer that sends every write to all of its destinations.

solid answer

~40 s

`io.MultiReader(r1, r2, ...)` is logical concatenation: reads come from `r1` until it returns `io.EOF`, then from `r2`, and the combined reader reports `io.EOF` only when the last source is exhausted. `io.MultiWriter(w1, w2, ...)` is fan-out: each `Write` copies the same bytes to every destination, in argument order, on the calling goroutine — nothing runs concurrently. Neither one owns its arguments: `MultiReader` never closes a source it has finished with, and `MultiWriter` never closes a destination, so lifetimes stay with whoever created them. The fan-out is strict rather than best effort: if a destination returns an error, or accepts fewer bytes than it was given, the `MultiWriter` stops there and returns that failure, and the destinations after it never see those bytes.

code

go · 5 lines
go
var log bytes.Buffer
h := sha256.New()

w := io.MultiWriter(&log, h)
fmt.Fprint(w, "payload") // both the buffer and the hash see "payload"

go deeper

for a junior

Be ready to say in one sentence which direction each one runs: MultiReader joins several sources into one stream, MultiWriter copies one stream to several destinations. Name a real use for each, such as prepending a header or mirroring output to a log.

for a middle

Explain the mechanics: sources are read strictly in order until EOF, destinations are written in argument order on the calling goroutine, and neither combinator closes or buffers anything. Know that a failing or short-writing destination aborts the fan-out at that point.

for a senior

Show that you plan ownership before you compose. Say who closes each source, which destination is essential and which is best-effort, and how you keep an optional sink from taking down a transfer it was only meant to observe.

for a principal

Frame it as an API decision: a function that accepts io.Reader composes with anything, while one that demands a concrete type or a ReadCloser forces callers into adapters. Argue for the smallest interface a package can accept without hiding lifetimes.

## Two shapes of composition Go's `io` package is deliberately tiny: a `Reader` has one method, `Read(p []byte) (int, error)`, and a `Writer` has one method, `Write(p []byte) (int, error)`. Because the interfaces are that small, the package can ship a handful of *combinators* — functions that take readers or writers and return a new one — and those combinators compose with anything, including types you wrote yourself. `io.MultiReader` and `io.MultiWriter` are the two most basic ones, and they run in opposite directions. ### io.MultiReader — concatenation `io.MultiReader(readers ...io.Reader) io.Reader` returns a single reader that presents its inputs as one continuous stream. It calls `Read` on the first source; when that source returns `io.EOF` it moves to the second, and so on. Only after the last source has returned `io.EOF` does the combined reader itself return `io.EOF`. This is the streaming equivalent of concatenating files: you can prepend a header you build in memory to a body you are streaming off disk, or replay a few bytes you already consumed by putting them in front of the original reader. Two properties matter. First, it is sequential, not parallel — the sources are read strictly in order, one at a time, and there is no goroutine anywhere in it. Second, it takes `io.Reader`, not `io.ReadCloser`, so it has no way to close anything and does not try. If your sources are open files or response bodies, closing them is still the caller's job. ### io.MultiWriter — fan-out `io.MultiWriter(writers ...io.Writer) io.Writer` returns a writer whose `Write` hands the same slice to each destination in turn. A single call to `Write` on the fan-out becomes one `Write` per destination, executed on the calling goroutine, in the order the arguments were given. Typical uses are mirroring a stream to a file and to a checksum at once, echoing a command's output to both a log buffer and the terminal, or teeing bytes into a size counter. The failure rule is the part people get wrong. The implementation loops over its destinations and, for each one, checks the result: if a destination returns a non-nil error, the loop stops immediately and the fan-out returns that error; if a destination returns fewer bytes than it was handed with a nil error, the fan-out stops and returns `io.ErrShortWrite`. Either way, destinations later in the list never receive those bytes for that call, and the caller — usually a copy loop — sees a write error and abandons the whole transfer. One failing sink kills the transfer for all of them. That is a reasonable default (silent data loss would be worse) but it means an optional destination, such as an audit log, must be wrapped in something that tolerates failure rather than dropped straight into the argument list. One small bonus: the value returned by `io.MultiWriter` also implements `io.StringWriter`, so writing a string through it does not have to allocate a byte slice for every destination. ### Adapting a Reader that is not a ReadCloser A related adapter lives in the same package. `io.NopCloser(r io.Reader) io.ReadCloser` wraps a reader so it satisfies an API that insists on `io.ReadCloser` — a common need when you are building a request body from a `strings.Reader` or a `bytes.Reader`, which have nothing to release. Its `Close` simply returns `nil`. It is the right tool when there genuinely is nothing to clean up, and the wrong one when there is: wrapping something that owns a file descriptor or a socket in `io.NopCloser` turns a leak into a leak nobody can see, because the caller believes it closed the stream. ### Reading the composition Because every combinator returns the same interface it consumed, these stack. `io.MultiReader(header, io.MultiReader(a, b))` is legal and behaves exactly as flattened. A fan-out can contain another fan-out. Nothing in the chain buffers on its own, nothing spawns goroutines, and nothing closes anything — which is why the whole family is cheap and predictable, and why every ownership question (who closes, who buffers, who tolerates a failure) stays with the code that assembled the chain.

  • Does io.MultiReader close a source once it has been read to EOF?
    No. It accepts `io.Reader`, which has no `Close` method, so it cannot close anything and does not try. Once a source returns `io.EOF` it simply stops calling it and moves on. If your sources are files or HTTP response bodies, the code that opened them still has to close them, usually with a `defer` at the call site.
  • You have an io.Reader but the API wants an io.ReadCloser. What does io.NopCloser give you, and when is it wrong?
    `io.NopCloser(r)` returns an `io.ReadCloser` that reads from `r` and whose `Close` returns `nil`. It is right when the reader holds no resource — a `strings.Reader`, a `bytes.Reader`, an in-memory buffer. It is wrong when something underneath does need releasing: the caller will dutifully call `Close`, nothing will happen, and you have hidden a file-descriptor or connection leak behind an interface that looks correct.
  • One destination in an io.MultiWriter is far slower than the rest. What does that cost?
    Everything, because the destinations are written sequentially on the calling goroutine. Each `Write` on the fan-out costs the sum of all its branches, so the slowest one sets the pace of the entire transfer. If a slow sink must not apply backpressure, give it its own goroutine fed by a bounded channel and put that hand-off — not the sink itself — into the `io.MultiWriter`.

MultiReader is gluing several tapes end to end so a single player runs through them; MultiWriter is carbon paper — one stroke of the pen marks every sheet in the stack, and if one sheet jams you stop pressing.

saying these in an interview costs you the question

  • Thinks io.MultiReader reads its sources concurrently
  • Believes io.MultiWriter writes to each destination in its own goroutine
  • Expects MultiReader to close each source after it returns EOF
  • Assumes a failing destination is skipped and the rest still get the bytes
  • Uses io.NopCloser on a reader that really owns a file or socket