The Writer Interface
Most languages give you a dozen different ways to send bytes somewhere: one API
for files, another for network sockets, another for in-memory buffers. Go has
essentially one. It's called io.Writer, and it is the whole definition:
type Writer interface {
Write(p []byte) (n int, err error)
}
That's it. A single method. Write takes a slice of bytes, tries to write them
somewhere, and reports how many bytes it actually wrote (n) and whether
something went wrong (err). Anything with that one method is a Writer.
Why one method matters
Because the interface is tiny, an enormous number of types satisfy it — and you
never had to plan for that. os.Stdout is a Writer. So is an open file, a
network connection, an HTTP response, a bytes.Buffer that collects bytes in
memory, and a strings.Builder that assembles a string. None of them
coordinated; each just happens to have a Write method.
The payoff: any function you write against io.Writer works with all of them.
Write your output logic once, and the caller decides where the bytes land — the
terminal, a file, a buffer in a test. This is Go's "small interface" philosophy
in one picture, and it is why the standard library composes so well.
Writing to any Writer
You rarely call Write by hand. The fmt package has a family of functions
whose first argument is a Writer — the F prefix means "to a writer":
package main
import (
"fmt"
"os"
)
func main() {
fmt.Fprintf(os.Stdout, "Hello, %s!\n", "Gopher")
fmt.Fprintln(os.Stderr, "this goes to standard error")
}
fmt.Println("hi") is really just fmt.Fprintln(os.Stdout, "hi") with the
destination baked in. Once you see that, "print" stops being magic: it's a
write to a particular Writer.
For plain strings, io.WriteString(w, s) is the direct route — it writes the
string to w without the formatting machinery.
Swapping the destination
Here's the same output logic pointed at a buffer instead of the screen:
package main
import (
"bytes"
"fmt"
)
func main() {
var buf bytes.Buffer
fmt.Fprintf(&buf, "captured: %d\n", 42)
fmt.Print(buf.String()) // captured: 42
}
Nothing about the Fprintf call changed — only the Writer did. That swap is
exactly how tests capture output, how you build up a response body, and how you
tee the same bytes to two places at once (a trick we'll reach in a later
lesson).
Implementing your own Writer
Since a Writer is "any type with a Write method", you can make one. Give a
type that method and it slots into every function above:
type upperWriter struct{}
func (upperWriter) Write(p []byte) (int, error) {
// (imagine uppercasing p here, then forwarding it)
return len(p), nil
}
The contract to honor: return the number of bytes you accepted, and return an
error only if you couldn't accept them all. Returning n short of len(p)
tells callers like fmt.Fprintf that the write failed partway — so a Writer
that "succeeds" must return n == len(p).
The exercise puts you on the other side of the interface: you'll build a Writer
that counts every byte written through it, and watch fmt.Fprintf drive it
without knowing or caring that it's yours.
Practice
1. A Counting Writer
IntermediateA Writer doesn't have to send bytes anywhere — it just has to account for them. Implement a Writer that counts every byte written through it.
Give the
countingWritertype aWritemethod with this exact signature:type countingWriter struct { Count int } func (c *countingWriter) Write(p []byte) (int, error)- Add
len(p)toc.Counton every call. - Return
(len(p), nil)— you accepted all the bytes, so report the full count and no error.
The method must be on a pointer receiver (
*countingWriter) so that updatingCountaffects the original value.Once it satisfies
io.Writer, anything that writes to a Writer will drive it. The tests write to it directly and throughfmt.Fprintf(&cw, ...), then check thatcw.Countequals the total number of bytes that passed through.package main import "fmt" // countingWriter is an io.Writer that records how many bytes are written // through it. It sends the bytes nowhere — it only counts them. type countingWriter struct { Count int } // Write accepts every byte in p and adds len(p) to Count. func (c *countingWriter) Write(p []byte) (int, error) { // TODO: count the bytes written — add len(p) to c.Count return len(p), nil } func main() { var cw countingWriter cw.Write([]byte("hello")) fmt.Fprintf(&cw, " %s!", "world") fmt.Println("bytes written:", cw.Count) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Add