32. The standard library's interfaces

📖 Reading · 10 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

You'll define a few interfaces of your own. You'll satisfy the standard library's constantly. These four are worth knowing by heart.

fmt.Stringer — how your type prints

package main

import "fmt"

type Duration struct {
    Minutes int
}

func (d Duration) String() string {
    h := d.Minutes / 60
    m := d.Minutes % 60
    if h == 0 {
        return fmt.Sprintf("%dm", m)
    }
    return fmt.Sprintf("%dh%02dm", h, m)
}

func main() {
    d := Duration{Minutes: 145}

    fmt.Println(d)
    fmt.Printf("%v | %s\n", d, d)
    fmt.Println([]Duration{{30}, {90}, {200}})
}

One method, and every printing path in the language uses it — inside slices, inside maps, in log output. It's the cheapest polish you can add to a type.

error — just an interface

The error type isn't special machinery. It's this:

type error interface {
    Error() string
}

So any type with an Error() string method is an error:

package main

import "fmt"

type ValidationError struct {
    Field string
    Value string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("invalid %s: %q", e.Field, e.Value)
}

func validate(field, value string) error {
    if value == "" {
        return &ValidationError{Field: field, Value: value}
    }
    return nil
}

func main() {
    if err := validate("email", ""); err != nil {
        fmt.Println("got:", err)

        if v, ok := err.(*ValidationError); ok {
            fmt.Println("the bad field was:", v.Field)
        }
    }

    fmt.Println(validate("email", "ada@example.com"))
}

A custom error is just a type with a method, and a type assertion gets your structured data back out. Note the pointer receiver and the & on the return — that's the convention for custom error types, and the errors module explains why it matters.

Note also that fmt.Println(err) printed the message: error and Stringer work the same way, and fmt checks for Error() string first.

sort.Interface — three methods, any ordering

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }

func main() {
    people := []Person{
        {Name: "Ada", Age: 36},
        {Name: "Grace", Age: 45},
        {Name: "Alan", Age: 41},
    }

    sort.Sort(ByAge(people))
    for _, p := range people {
        fmt.Printf("%-6s %d\n", p.Name, p.Age)
    }
}

ByAge is a named slice type — module 5's trick — carrying the three methods sort.Sort needs. ByAge(people) is a conversion, not a copy of the data: same backing array, so sorting through it sorts people.

In modern code you'd write sort.Slice(people, func(i, j int) bool {...}) and skip the type. sort.Interface is still worth understanding, because it shows how an interface turns "an algorithm" into something reusable — and because you'll read plenty of code that predates sort.Slice.

io.Writer — the most useful interface in Go

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

Files, network connections, HTTP responses, gzip compressors, hash functions and in-memory buffers all satisfy it. Which means a function that writes to an io.Writer works with all of them:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func report(w io.Writer, items []string) {
    fmt.Fprintf(w, "%d items\n", len(items))
    for i, item := range items {
        fmt.Fprintf(w, "%d. %s\n", i+1, item)
    }
}

func main() {
    items := []string{"slices", "maps", "interfaces"}

    report(os.Stdout, items)

    var buf bytes.Buffer
    report(&buf, items)
    fmt.Println("captured", len(buf.String()), "bytes")
    fmt.Print(strings.ToUpper(buf.String()))

    report(io.Discard, items)
}

The same report wrote to the terminal, into memory, and into the void — without knowing anything about any of them. Note fmt.Fprintf: every fmt printing function has an F variant that takes a writer first. fmt.Printf is literally fmt.Fprintf(os.Stdout, ...).

Take an io.Writer instead of printing directly. It's the single easiest habit that makes Go code testable — the test passes a bytes.Buffer and asserts on the string.

Implementing io.Writer yourself

Because it's one method, you can write your own sink in a few lines:

package main

import (
    "fmt"
    "strings"
)

type UpperWriter struct {
    sb strings.Builder
}

func (u *UpperWriter) Write(p []byte) (int, error) {
    u.sb.WriteString(strings.ToUpper(string(p)))
    return len(p), nil
}

func main() {
    u := &UpperWriter{}

    fmt.Fprintf(u, "hello %s\n", "world")
    fmt.Fprintln(u, "second line")

    fmt.Print(u.sb.String())
}

Write must return how many bytes it consumed and an error. Return len(p), nil when you handled everything — returning less without an error is a protocol violation that will confuse callers.

That's the shape of a gzip writer, a line-counting writer, a tee-to-two-places writer. Composition all the way down.

Your turn

Give Money a String() method that formats cents as dollars, so the program prints:

$19.99
total: $24.98
package main

import "fmt"

type Money struct {
    Cents int
}

// add String() string — format as $D.CC

func main() {
    price := Money{Cents: 1999}
    shipping := Money{Cents: 499}

    fmt.Println(price)
    fmt.Println("total:", Money{Cents: price.Cents + shipping.Cents})
}
package main

import "fmt"

type Money struct {
    Cents int
}

func (m Money) String() string {
    return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}

func main() {
    price := Money{Cents: 1999}
    shipping := Money{Cents: 499}

    fmt.Println(price)
    fmt.Println("total:", Money{Cents: price.Cents + shipping.Cents})
}

Interfaces done. Next module: the one thing every Go function seems to return — errors.