Demystifying Golang's io.Reader and io.Writer Interfaces

If you’re coming to Go from a more flexible, dynamically typed language like Ruby or Python, there may be some confusion as you adjust to the Go way of doing things. In my case, I had some trouble wrapping my head around io.Reader, io.Writer, io.ReadCloser etc. What are they used for, and how can they be included in our Go programs for interesting and helpful results?

Quick interface review

To make up for some of the flexibility lost by not having generics, and for other reasons as well, Go provides an abstraction in the form of interfaces.

You can specify an interface and then any consumer of that interface will accept it.

type error interface {
    Error() string

Many standard library components of Go define interfaces. In fact, the error type you know and love (hate?) is simply an interface which insists that a method named Error which consumes nothing and returns a string must be defined on a struct for the interface to count as satisfied. Interfaces in Go are set implicitly, so all you have to do is define the required methods on your type and it will qualify as implementing that interface.

For instance:

package main

import (

type Animal interface {
    Say() string

type Person struct {

func (p Person) Say() string {
    return "Hey there bubba!"

func (p Person) Greet(animalToGreet Animal) {

type Dog struct {
    age int
    breed string
    owner *Person

func (d Dog) Say() string {
    return "Woof woof!"

func (d Dog) Growl() {

func (d *Dog) Snuggle() {
    // snuggle code...

func (d Dog) Sniff(animalToSniff Animal) (bool, error) {
    // sniff code...
    return true, nil

func (d Dog) Greet(animalToGreet Animal) {
    if _, ok := animalToGreet.(Person); ok {
    } else {
        friendly, err := d.Sniff(animalToGreet)
        if err != nil {
            fmt.Fprintln(os.Stderr, "Error sniffing a non-person")
        if !friendly {

func main() {
    d1 := Dog{2, "shibe", &Person{}}
    d2 := Dog{3, "poodle", &Person{}}
    fmt.Println("Successfully greeted a dog.")

Run here:

Yup, I “went there” with the Animal OO-ish (Go doesn’t have pure objects) cliché.

When you compile a program containing the above, the Go compiler knows that the Dog struct satisfies the Animal interface provided (it infers this because Dog implements the neccesary methods to qualify), so it won’t complain if you pass instances of of Dog to functions which demand an Animal type. This allows for a lot of power and flexibility in your architecture and abstractions, without breaking the type system.

So what’s with io?

io is a Golang standard library package that defines flexible interfaces for many operations and usecases around input and output.


You can use the same mechanisms to talk to files on disk, the network, STDIN/STDOUT, and so on. This allows Go programmers to create re-usable “Lego brick” components that work together well without too much shimming or shuffling of components. They smooth over cross-platform implemenation details, and it’s all just []byte getting passed around, so everyone’s expectations (senders/writers and receivers/readers) are congruent. You have io.Reader, io.ReadCloser, io.Writer, and so on to use. Go also provides packages called bufio and ioutil that are packed with useful features related to using these interfaces.

OK, but what can you do with it.

Let’s look at an example to see how combining some of these primitives can be useful in practice. I’ve been working on a project where I want to attach to multiple running Docker containers concurrently and stream (multiplex) their output to STDOUT with some metadata (container name) prepended to each log line. Sounds easy, right? ;)

The Docker REST API bindings written by fsouza provide an abstraction whereby we can pass an io.Writer instance for STDOUT and STDERR of the container we are attaching to. So we have control of a io.Writer that we inject in, but how do we read what gets written by this container one line at a time, and multiplex/label the output together in the fashion I described in the previous paragraph?

We will use a combination of Go’s concurrency primitives, io.Pipe, and a bufio.Scanner to accomplish this.

Since the call to the API binding’s AttachContainer method hijacks the HTTP connection and consequently forces the calling goroutine to be blocked, we run each Attach call in its own goroutine.

We need an io.Reader to be able to read and parse the output from the container, but we only have the option to pass in an instance of io.Writer for STDOUT and STDERR. What to do? We can use a call to io.Pipe (see here for reference). io.Pipe returns an instance of a PipeReader, and an instance of a PipeWriter, which are connected (calling the Write method on the Writer will lead directly to what comes out of Read in the Reader). So, we can use the returned Reader to stream the output from the container.

The final step is to use a bufio.Scanner to read the output from the PipeReader line by line. We have already generated the prefix earlier and saved it in the Service struct we are working with (Service in my implementation is a very light wrapper around a container).

Therefore, the final method looks like this:

func (s *Service) Attach() error {
    r, w := io.Pipe()
    options := apiClient.AttachToContainerOptions{
        Container:    s.Name,
        OutputStream: w,
        ErrorStream:  w,
        Stream:       true,
        Stdout:       true,
        Stderr:       true,
        Logs:         true,
    fmt.Println("Attaching to container", s.Name)
    go s.api.AttachToContainer(options)
    go func(reader io.Reader, s Service) {
        scanner := bufio.NewScanner(reader)
        for scanner.Scan() {
            fmt.Printf("%s%s \n", s.LogPrefix, scanner.Text())
        if err := scanner.Err(); err != nil {
            fmt.Fprintln(os.Stderr, "There was an error with the scanner in attached container", err)
    }(r, *s)
    return nil

We kick off attaching to, and reading from, the container at the same time- when the attach is complete and starts streaming, the scanner.Scan loop will start logging.


I had some trouble understanding io.Writer, io.Reader, etc. when getting started with Go (and recently as well), but I think I was over-thinking their simplicity and explicit power. Additionally, learning about some higher-level abstractions related to them helped a lot. Hopefully this article is useful for you and clears stuff up in the future. I know that my Go has accelerated a lot since grokking these concepts, especially since so much (file IO etc.) relies on it.

Until next time, stay sassy Internet.

  • Nathan
I want to help you become an elite engineer. Subscribe to follow my work with containers, observability, and languages like Go, Rust, and Python on Gumroad.

If you find a mistake or issue in this article, please fix it and submit a pull request on Github (must be signed in to your GitHub account).

I offer a bounty of one coffee, beer, or tea for each pull request that gets merged in. :) Make sure to cc @nathanleclaire in the PR.