skip to content

Your function takes an io.Writer. How do you detect at run time that it also implements io.ReaderFrom, and what must you do when it does not?

level: juniorimportance: should knowfreq 45%

answer

  1. the value may do more than promised
  2. ask at run time, not at compile time
  3. two results, never one, so no panic
  4. assert to a second, wider interface
  5. the slow path must still exist

basics

~20 s

Use the comma-ok type assertion: rf, ok := w.(io.ReaderFrom). If ok is true, call rf.ReadFrom for the fast path; if not, fall back to ordinary Write calls. An optional interface is an optimisation, so the fallback is mandatory.

solid answer

~50 s

The value you hold is an `io.Writer`, but at run time it carries a concrete dynamic type that may have more methods than `Write`. A comma-ok assertion asks about that dynamic type: `if rf, ok := w.(io.ReaderFrom); ok { return rf.ReadFrom(r) }`. When the assertion succeeds you hand the whole transfer to the writer, which may push it through a much cheaper path; `*bytes.Buffer`, `*bufio.Writer`, `*os.File` and `*net.TCPConn` all implement `io.ReaderFrom`. When it fails, `ok` is false and `rf` is nil — no panic, no error — and you must still do the job with plain `Write` calls. That is the whole discipline of capability probing: the declared parameter type is the contract, the probe is a speed-up, and both paths must produce the same result. Never document the fast path as something callers have to supply.

code

go · 6 lines
go
func send(w io.Writer, r io.Reader) (int64, error) {
	if rf, ok := w.(io.ReaderFrom); ok {
		return rf.ReadFrom(r) // fast path: the writer drives the read
	}
	return io.Copy(w, r) // fallback: an ordinary buffered loop
}

go deeper

for a junior

Recall the comma-ok assertion to a second interface and that a failed probe yields false plus a nil value rather than a panic. Be ready to write both branches on a whiteboard.

for a middle

Explain why the interface value carries a dynamic type at all, why matching is structural and exact, and why the fast and slow paths must be observably identical apart from speed.

for a senior

Show the judgment about when to probe: cheap check, real measured win, no behaviour change, and no contract promise that a wrapper elsewhere could quietly break.

for a principal

Frame it as an extension mechanism for interfaces you can never change, and be ready to say when a capability should stop being optional and move into the declared signature instead.

## What is actually being asked A Go interface value is a pair: a dynamic type and a value of that type. When a caller passes you an `io.Writer`, the static type in your signature says only that the value has a `Write([]byte) (int, error)` method. The concrete thing behind it — a `*bytes.Buffer`, a `*net.TCPConn`, a `*bufio.Writer`, some type from another module — almost always has *more* methods than that. **Optional capability probing** is the idiom of asking, at run time, whether the dynamic type happens to have an extra capability you can exploit, while still working correctly when it does not. ## The mechanics The question is asked with a type assertion to a second interface type: ```go if rf, ok := w.(io.ReaderFrom); ok { return rf.ReadFrom(r) } ``` The two-result ("comma-ok") form never panics. If the dynamic type's method set includes `ReadFrom(io.Reader) (int64, error)`, then `ok` is true and `rf` is the same underlying value viewed through the wider interface. If it does not, `ok` is false and `rf` is the nil interface value. Nothing else changes — you have not converted anything, you have only asked a question. The method set is matched **structurally and exactly**. There is no registration and no `implements` keyword; the type qualifies because it has methods with those exact names and signatures. A method named `ReadFrom` with a different signature does not qualify. ## Why the standard library is built this way `io.Writer` has exactly one method and it is frozen forever. That minimalism is what makes it universally implementable, but it also means the interface cannot express "and this particular writer can swallow an entire reader in one kernel call". Optional interfaces are how Go extends a frozen interface without breaking anyone: a new optional interface can be introduced at any time, because implementations that lack it simply fail the probe. `io.ReaderFrom` and `io.WriterTo` exist precisely so that bulk copies can bypass a byte-shuffling loop; `io.Copy` performs this exact probe internally, which is why in real code the fallback is usually just a call to `io.Copy`. ## The rules that make a probe correct 1. **The fallback is not optional.** If your function only works when the probe succeeds, then the capability was never optional and belongs in your parameter's declared type instead. 2. **Both paths must be observably equivalent apart from speed.** If the fast path writes different bytes, or reports errors differently, callers will see behaviour change depending on which writer they passed — a bug that only appears in production, with a specific type. 3. **Probe once, not per iteration.** The check is cheap (a comparison against the value's type descriptor), but doing it inside a hot loop is pointless; hoist it out. 4. **Use the comma-ok form.** The single-result form, `w.(io.ReaderFrom)`, panics when the value does not satisfy the interface. That is the right form only when a failure is a programming error, which a capability probe never is. 5. **Do not advertise the fast path in your contract.** The moment your docs say "pass an `io.ReaderFrom` for speed", callers start depending on it and any wrapper in the chain that hides the capability becomes a silent regression. ## The trap to know about A probe asks about the value you are actually holding. If something between the original writer and you has wrapped it — a struct that embeds the writer to count bytes, tee it to a log, or add a mutex — then the dynamic type is the *wrapper*, and the wrapper does not implement `io.ReaderFrom` unless its author wrote that method. The probe returns false, the fast path silently disappears, and nothing anywhere reports an error. That is the characteristic failure of this whole idiom: it degrades quietly. ## A worked shape ```go func send(w io.Writer, r io.Reader) (int64, error) { if rf, ok := w.(io.ReaderFrom); ok { return rf.ReadFrom(r) } return io.Copy(w, r) } ``` Written out like that, the function is redundant — `io.Copy` already makes the same check — and recognising that is part of understanding the idiom. You write the probe yourself when the capability is one no standard helper knows about: a `Flush() error`, a `CloseWrite() error`, a `Name() string`. The shape stays identical: ask, use it if it is there, do the ordinary thing if it is not.

  • Which standard library types commonly satisfy io.ReaderFrom?
    `*bytes.Buffer`, `*bufio.Writer`, `*os.File` and `*net.TCPConn` all have a `ReadFrom` method. For the network and file types the win is large, because the kernel can move bytes without them passing through your process; for a buffer the win is avoiding a second copy and the intermediate allocation.
  • If the probe fails, should the function return an error asking the caller for a better writer?
    No. The parameter type is `io.Writer`, so every `io.Writer` is a valid argument by definition. Returning an error would turn an optimisation into a requirement and break callers who passed a perfectly good writer. If the capability really is required, change the signature to demand it, so the compiler rejects the bad call instead of the run time.
  • Does the probe itself cost anything worth worrying about?
    Barely. An assertion to an interface type is a run-time lookup of the concrete type's method table, cached per type pair, and it is nothing beside the I/O it guards. The one thing to avoid is repeating it inside a per-chunk loop: do it once at the top of the function and branch on the result.

You ask the courier whether they happen to have a van before handing over the pallet. If they do, it goes in one trip; if not, you still carry the boxes yourself.

saying these in an interview costs you the question

  • Says the fallback path is optional
  • Uses the single-result assertion and panics on ordinary writers
  • Believes the compiler chooses the fast path
  • Assumes every io.Writer has a ReadFrom method
  • Documents the fast path as part of the function's contract
  • Puts the probe inside the copy loop