Composing Readers and Writers
The last two lessons each introduced a one-method interface. On their own they'd be a curiosity. The reason they're the backbone of Go's I/O is what happens when you connect them: because everything is a Reader or a Writer, a handful of tiny functions can plug any source into any destination. This lesson is the payoff.
io.Copy: stream anything into anything
You have a Reader (a source of bytes) and a Writer (a place to put them). To
move all the bytes from one to the other, you don't write a loop — you call
io.Copy:
package main
import (
"io"
"os"
"strings"
)
func main() {
src := strings.NewReader("stream me\n")
io.Copy(os.Stdout, src) // dst first, then src
}
io.Copy(dst, src) reads from src until EOF and writes everything to dst,
in bounded chunks — so it moves a gigabyte file without loading a gigabyte into
memory. The signature reads destination-first, matching assignment (dst = src).
Because both parameters are interfaces, the same call copies a file to a
socket, a socket to a buffer, or a string to stdout. One function, every
combination — that's the whole argument for small interfaces made concrete.
io.MultiWriter: fan one write out to many
Sometimes you want the same bytes in two places — printed and logged, say.
io.MultiWriter bundles several Writers into one:
package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
var log bytes.Buffer
w := io.MultiWriter(os.Stdout, &log)
fmt.Fprintln(w, "important event")
// printed to the screen AND captured in log
fmt.Print("logged copy: ", log.String())
}
Every write to w is forwarded to each underlying Writer in turn — this is the
Unix tee command as a value. The result is itself an io.Writer, so it drops
straight into fmt.Fprintf, io.Copy, or anything else expecting one.
Wrapping: the decorator pattern
Here's the deeper move. Because a Reader can be built from another Reader, you
can wrap one to add behavior while keeping the same interface. The standard
library ships io.TeeReader(r, w): it returns a Reader that behaves exactly like
r, but quietly copies everything it reads into w as a side effect.
tee := io.TeeReader(src, &spyBuffer)
io.ReadAll(tee) // reads src normally; spyBuffer now holds a copy
Whoever consumes tee sees a plain Reader and never notices the extra behavior
— which is the point. This is the decorator pattern, and Go leans on it
constantly: bufio.NewReader wraps a Reader to add buffering, gzip.NewReader
wraps one to add decompression. Each layer takes a Reader and returns a Reader,
so they stack without any layer knowing about the others.
Why this is the Go way
None of these functions required inheritance, generics gymnastics, or a class hierarchy. They work because the interfaces are small enough that unrelated types satisfy them by accident, and small enough that wrapping one is trivial. A big interface — twenty methods — would be satisfied by almost nothing and tedious to wrap. Go bets on the opposite, and I/O is where you feel it win.
Your exercise is a small piece of this world: a broadcast function that sends
one message to any number of Writers at once, using io.MultiWriter. Written
against the interface, it neither knows nor cares whether those Writers are
buffers, files, or sockets.
Practice
1. Broadcast to Many Writers
IntermediateSend one message to many destinations at once. Implement
broadcast:func broadcast(msg string, writers ...io.Writer) (int, error)- Bundle all the
writersinto a single Writer withio.MultiWriter(writers...). - Write
msgto that combined Writer once, usingio.WriteString. - Return the count and error from that single write.
Every writer receives the full message —
io.MultiWriterfans the write out to each one in turn, like the Unixteecommand.Notes:
writersis variadic, sobroadcast("x")is legal — zero writers. It must not panic;io.MultiWriter()with no arguments is a valid Writer that discards, and writing to it still returns(len(msg), nil).- On success the returned count equals
len(msg). - Because the parameters are
io.Writer, the destinations can be buffers, files, sockets, oros.Stdout—broadcastneither knows nor cares.
package main import ( "bytes" "fmt" "io" ) // broadcast writes msg to every writer at once using io.MultiWriter, and // returns the count and error from that single write. func broadcast(msg string, writers ...io.Writer) (int, error) { // TODO: combine writers with io.MultiWriter and write msg with io.WriteString return 0, nil } func main() { var a, b bytes.Buffer n, err := broadcast("hello", &a, &b) fmt.Printf("wrote %d bytes, err=%v, a=%q b=%q\n", n, err, a.String(), b.String()) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Bundle all the