Which optional interfaces does io.Copy probe before falling back to its buffer loop, and in what order?
answer
- two probes before any bytes move
- one interface per direction of the copy
- the source is asked first
- the fallback allocates a temporary buffer
- a supplied buffer can go unused
basics
~20 sio.Copy first asks whether the source implements io.WriterTo and calls src.WriteTo(dst); otherwise whether the destination implements io.ReaderFrom and calls dst.ReadFrom(src); only if neither matches does it loop through its own buffer, 32 KiB by default.
solid answer
~40 s`io.Copy(dst, src)` makes two capability probes before it does any work of its own. It asserts `src.(io.WriterTo)` first and, on success, returns `src.WriteTo(dst)`. Failing that it asserts `dst.(io.ReaderFrom)` and returns `dst.ReadFrom(src)`. Only when both probes fail does it allocate a temporary buffer — 32 KiB in the current implementation — and shuttle bytes with `Read` and `Write`. The order matters: if both sides advertise a capability, the source wins. This is why implementing `WriteTo` on a reader-shaped type, or `ReadFrom` on a writer-shaped type, silently makes every `io.Copy` in every package faster for your type, and why `*net.TCPConn` and `*os.File` can move data without it entering your process. `io.CopyBuffer` makes exactly the same probes; if a fast path is taken, the buffer you supplied is never touched.
code
go · 16 linestype table struct {
rows [][]byte
}
// io.Copy calls this instead of draining the type through Read.
func (t *table) WriteTo(w io.Writer) (int64, error) {
var n int64
for _, row := range t.rows {
m, err := w.Write(row)
n += int64(m)
if err != nil {
return n, err
}
}
return n, nil
}go deeper
Recall that io.Copy is not just a loop: it first asks each side whether it can do the whole transfer itself, and only then falls back to reading and writing chunks.
Be able to state both interface names, their signatures and the order of the probes, and explain how adding one method to your own type changes the behaviour of copies you never wrote.
Show that you know where the real wins come from — sockets and files handing the transfer to the kernel — and that a wrapper without the forwarding method quietly removes them.
Treat the pair as the model for extending a frozen interface: new capability, no signature change, silent fallback, and a documented obligation on anyone shipping wrappers.
## The two interfaces Two one-method interfaces in `io` exist purely as optional capabilities: ```go type WriterTo interface { WriteTo(w Writer) (n int64, err error) } type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error) } ``` Neither is ever required. They say: *I know a better way to move a whole stream than one buffer at a time — let me do it.* ## What io.Copy does, in order 1. Assert the **source** to `io.WriterTo`. If that succeeds, `io.Copy` returns `src.WriteTo(dst)` and does nothing else. 2. Otherwise assert the **destination** to `io.ReaderFrom`. If that succeeds, it returns `dst.ReadFrom(src)`. 3. Otherwise allocate a temporary buffer (32 KiB in the current implementation, or smaller when the source is a limited reader) and loop: `Read` into the buffer, `Write` what came back, until EOF or error. The ordering is a real, observable rule, not an implementation detail you can ignore: when both sides advertise a capability, the source's `WriteTo` runs and the destination's `ReadFrom` is never called. If you implement both on the same type and they behave differently, that asymmetry becomes a bug that depends on which side of a copy your value lands. ## Why it matters for your own types Because `io.Copy` probes rather than requires, adding one method to your type opts it into every copy anyone writes, with no coordination and no API change: * A reader-shaped type that already has the bytes contiguous in memory implements `WriteTo` and hands them over in one `Write` instead of being drained 32 KiB at a time. * A writer-shaped type backed by something that can pull implements `ReadFrom`. In the standard library this is where the large wins live: `*bytes.Buffer` implements both, `*bufio.Writer` implements `ReadFrom`, `*os.File` and `*net.TCPConn` implement `ReadFrom` so that on Linux the kernel can move bytes between two descriptors without them being copied into and out of your address space. None of that is visible in `io.Copy`'s signature. ## The buffer, and io.CopyBuffer `io.CopyBuffer(dst, src, buf)` exists so a caller can supply and reuse the temporary buffer instead of letting each copy allocate one. It is documented as making the same two probes, and if either fast path is taken, `buf` is not used at all. Passing a megabyte buffer therefore does not force the generic loop, and does not make the fast path bigger — a common misreading. If your goal is to avoid the per-copy allocation in a hot loop, `io.CopyBuffer` with a reused buffer is right; if your goal is throughput on a socket or a file, the fast path was already doing better than any buffer size you can pick. ## Contract for your implementation If you implement either method, honour the documented contract: * `WriteTo` writes data to `w` **until there is no more data to write or an error occurs**, and returns the number of bytes written plus any error. * `ReadFrom` reads from `r` **until EOF or an error**, returns the count, and does **not** report `io.EOF` as an error — reaching the end is success. Both must leave the receiver in a sane state on partial failure and return an accurate byte count, because callers use it for progress and for content-length bookkeeping. ## The recursion trap The classic bug is implementing `WriteTo` in terms of `io.Copy` with the same value as the source: ```go func (t *thing) WriteTo(w io.Writer) (int64, error) { return io.Copy(w, t) // io.Copy probes t for WriterTo and calls this again } ``` That recurses until the stack dies. The fix is to hide the capability from the probe by copying from a value whose method set does not include `WriteTo` — wrapping the source in an anonymous struct that embeds only `io.Reader` is the standard trick — or to write the loop yourself. The mirror image applies to a `ReadFrom` implemented as `io.Copy(t, r)`. ## What to say in an interview Name the two interfaces, give the order (source first), say the fallback is a buffered loop with a 32 KiB default, and then make the point that matters: this is capability probing rather than a type switch on concrete types, so a type in a module `io` has never heard of gets the fast path automatically — and equally, anything that wraps a capable type without forwarding the method silently loses it.
- If the source implements io.WriterTo and the destination implements io.ReaderFrom, which one runs?The source's `WriteTo`. `io.Copy` asks the source first and returns immediately when that probe succeeds, so the destination's `ReadFrom` is never called. If your type implements both, keep them semantically identical, because which one executes depends on what the other side of the copy happens to be.
- Does passing your own buffer to io.CopyBuffer skip the probes?No. It makes the same two checks first, and the documentation is explicit that if either fast path is taken the supplied buffer is not used. A supplied buffer only replaces the temporary allocation in the generic loop; it never forces that loop to run.
- What goes wrong if a type implements WriteTo by calling io.Copy with itself as the source?Infinite recursion. `io.Copy` probes the source for `io.WriterTo`, finds the very method that called it, and calls it again until the stack is exhausted. Hide the capability by copying from a value that exposes only `Read` — an anonymous struct embedding `io.Reader` is the usual trick — or write the read/write loop directly.
saying these in an interview costs you the question
- Says io.Copy always allocates a 32 KiB buffer
- Thinks the destination is probed before the source
- Believes the compiler selects the fast path
- Assumes a buffer given to io.CopyBuffer is always used
- Implements WriteTo by calling io.Copy on the same value
- Reports io.EOF as an error from ReadFrom