James Allured

io.Reader and io.Writer in Go

Go interfaces are one of the language features I enjoy using the most. They’re so simple yet they’re incredibly powerful when trying to write clear and maintainable code. io.Reader and io.Writer are arguably two of the most common in Go code (apart from the Error interface) and they represent what’s so great about the humble interface.

If you want a quick primer on interfaces in Go, Go By Example is a great reference.

io.Reader

io.Reader is a single method interface that represents a type from which bytes can be read from.

type Reader interface {
    Read(p []byte) (n int, err error)
}

There are various examples of types in the Go standard library that implement the io.Reader interface such as buffers of bytes, HTTP request and response bodies, and files.

The interface itself encapsulates a Read method which takes a byte slice as an argument and returns an integer value representing the number of bytes read to the slice and an error value if one is encountered.

Read will read all of the bytes available in the stream into the provided byte slice until it either runs out of bytes to read or fills the byte slice. When the reader does run out of bytes to read, it will return the number of bytes read and an io.EOF error.

It is less common that you will directly use the Read method in your code. Instead, you’re much more likely to use the helper functions provided by various packages that expect an io.Reader and abstract the use of Read from you. One such example is the io.ReadAll function.

r := strings.NewReader("Hello, world!")

bytes, err := io.ReadAll(r)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("%s", bytes)
Hello, world!

Click here to run this example on the Go Playground

io.Writer

io.Writer is a single method interface that represents a type to which bytes can be written to.

type Writer interface {
    Write(p []byte) (n int, err error)
}

Again, we can see plenty of examples of types that implement io.Writer in Go’s standard library. HTTP request and response bodies, buffers of bytes, and files are just a few.

The io.Writer interface encapsulates a single Write method which takes a byte slice as an argument and returns an integer value representing the number of bytes written to the underlying type from the slice and an error value if one is encountered.

Write will write all of the bytes passed in the byte slice to the underlying type that implements the interface. If the number of bytes written is less than the length of the slice then the Write method will return a non-nil error to give the caller context as to why all of the bytes were not written. Something else to note here is that Write will never modify the byte slice provided as an argument.

Like io.Reader, you are much more likely to use helper functions provided by various packages that abstract the direct use of Write. One very common example is the fmt.Fprintf function.

s := "Hello world!"
fmt.Fprintf(os.Stdout, "%s, %d", s, len(s))
Hello world!, 12

Click here to run this example on the Go Playground

io.Writer and io.Reader Composition

One of the reasons that io.Reader and io.Writer are so powerful is that they are concise and encapsulate a single behaviour. This means that it’s easy to include them when defining new interfaces or combine them with other interfaces to tightly encapsulate more than one behaviour.

As an example, io.ReadWriter includes the behaviour of both the io.Reader and io.Writer interfaces.

type ReadWriter interface {
    Reader
    Writer
}

Another example is the io.ReadCloser interface, which encapsulates the behaviour of an io.Reader and io.Closer. The io.Closer interface is similar to io.Reader and io.Writer in that it wraps a single method Close which allows the caller to close the underlying type, such as a file or a network connection.

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

io.ReadCloser can be seen when working with net/http request body types. The Body field of the http.Request struct implements io.ReadCloser.

res, err := http.Get("https://http.cat/102")
if err != nil {
    log.Fatal(err)
}
defer res.Body.Close()

bytes, err := io.ReadAll(res.Body)
if err != nil {
    log.Fatal(err)
}

err = os.WriteFile("102.jpg", bytes, 0666)
if err != nil {
    log.Fatal(err)
}

The example shown above is correct as of Go 1.16. In this version, io/ioutil was deprecated and functionality provided by that package was moved into io and os (see Go 1.16 release notes). To make the above example compatible with older versions of Go, replace io.ReadAll with ioutil.ReadAll and os.WriteFile with ioutil.WriteFile.

Resources