31. Composing interfaces

📖 Reading · 11 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).

Small interfaces are the goal, but sometimes you need several capabilities at once. Go's answer isn't a bigger interface — it's embedding small ones into a combination, the same composition idea you saw with structs.

Embedding interfaces

package main

import "fmt"

type Reader interface {
    Read() string
}

type Writer interface {
    Write(s string) int
}

type ReadWriter interface {
    Reader
    Writer
}

type Memory struct {
    data string
}

func (m *Memory) Read() string { return m.data }
func (m *Memory) Write(s string) int {
    m.data += s
    return len(s)
}

func main() {
    var rw ReadWriter = &Memory{}

    rw.Write("hello ")
    rw.Write("world")
    fmt.Println(rw.Read())

    var r Reader = rw
    fmt.Println(r.Read())
}

ReadWriter lists two interfaces instead of two methods; it means "has everything Reader has, plus everything Writer has".

Then look at var r Reader = rw: a ReadWriter can be assigned to a Reader variable for free, because anything satisfying the bigger set necessarily satisfies the smaller one. Interfaces flow downward toward less capability, automatically.

This is exactly how the standard library is built. io.ReadWriter, io.ReadCloser and io.ReadWriteCloser are all just combinations of io.Reader, io.Writer and io.Closer.

Define the interface where you use it

The habit that makes Go codebases pleasant: a package declares the narrow interface it needs, rather than importing somebody else's wide one.

package main

import "fmt"

type Logger interface {
    Log(msg string)
}

type Metrics interface {
    Count(name string)
}

type ConsoleTools struct {
    prefix string
    counts map[string]int
}

func (c *ConsoleTools) Log(msg string) { fmt.Println(c.prefix + msg) }
func (c *ConsoleTools) Count(name string) {
    if c.counts == nil {
        c.counts = map[string]int{}
    }
    c.counts[name]++
}
func (c *ConsoleTools) Report(name string) int { return c.counts[name] }

func processOrder(id int, log Logger) {
    log.Log(fmt.Sprintf("processing order %d", id))
}

func main() {
    tools := &ConsoleTools{prefix: "[app] "}

    processOrder(1, tools)
    processOrder(2, tools)

    tools.Count("orders")
    tools.Count("orders")
    fmt.Println("orders counted:", tools.Report("orders"))
}

processOrder asks for a Logger — one method — even though the thing it gets can do much more. That's the smallest honest contract, so it's the easiest to satisfy in a test:

package main

import "fmt"

type Logger interface {
    Log(msg string)
}

type fakeLogger struct {
    lines []string
}

func (f *fakeLogger) Log(msg string) { f.lines = append(f.lines, msg) }

func processOrder(id int, log Logger) {
    log.Log(fmt.Sprintf("processing order %d", id))
}

func main() {
    fake := &fakeLogger{}

    processOrder(1, fake)
    processOrder(2, fake)

    fmt.Println("captured", len(fake.lines), "log lines")
    for _, l := range fake.lines {
        fmt.Println(" -", l)
    }
}

Eight lines and you have a test double. No mocking framework, no code generation — because the interface has one method and satisfaction is implicit. Design for that and testing stays cheap.

Embedding an interface in a struct

A struct can embed an interface, which means "I have these methods, forwarded to whatever is in this field":

package main

import "fmt"

type Store interface {
    Get(key string) string
    Put(key, value string)
}

type MapStore struct {
    m map[string]string
}

func (s *MapStore) Get(key string) string   { return s.m[key] }
func (s *MapStore) Put(key, value string)   { s.m[key] = value }

type LoggingStore struct {
    Store
}

func (l LoggingStore) Put(key, value string) {
    fmt.Printf("PUT %s=%s\n", key, value)
    l.Store.Put(key, value)
}

func main() {
    base := &MapStore{m: map[string]string{}}
    store := LoggingStore{Store: base}

    store.Put("lang", "go")
    store.Put("year", "2009")
    fmt.Println(store.Get("lang"))
}

LoggingStore satisfies Store while implementing only one of its methods — Get is promoted straight through from the embedded interface. That's the decorator pattern with no boilerplate, and it's how middleware gets written in Go.

Careful, though: if the embedded interface is nil and somebody calls a method you didn't override, it panics at runtime rather than failing to compile. Wrap what you're given, and don't leave the field empty.

The nil interface trap

Save this one; it will cost you an afternoon otherwise.

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{}

func (d *Dog) Speak() string { return "woof" }

func main() {
    var s Speaker
    fmt.Println("untouched interface is nil:", s == nil)

    var d *Dog
    s = d

    fmt.Println("after assigning a nil *Dog:", s == nil)
    fmt.Printf("type inside: %T\n", s)
}

The second line prints false. An interface value is a (type, value) pair, and it's nil only when both halves are nil. Assigning a nil *Dog gives you the pair (*Dog, nil) — a non-nil interface holding a nil pointer.

Where it bites is error returns:

type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }

func doWork() error {
    var err *MyError    // nil
    if somethingFails() {
        err = &MyError{"boom"}
    }
    return err          // ALWAYS non-nil as an error!
}

// caller: if err := doWork(); err != nil { ... }  <- always true

The fix is to return the concrete type only when it's real:

func doWork() error {
    if somethingFails() {
        return &MyError{"boom"}
    }
    return nil          // an untyped nil — genuinely nil
}

Rule of thumb: declare functions as returning error, and return a literal nil for success. Never return a typed nil pointer as an interface.

Your turn

Complete TimestampWriter so it satisfies Printer by embedding one and overriding Print to prefix the message. The program should print:

[t] hello
[t] world
package main

import "fmt"

type Printer interface {
    Print(msg string)
}

type Console struct{}

func (Console) Print(msg string) { fmt.Println(msg) }

type TimestampWriter struct {
    // embed a Printer here
}

// override Print to prefix "[t] " and delegate

func main() {
    w := TimestampWriter{Printer: Console{}}
    w.Print("hello")
    w.Print("world")
}
package main

import "fmt"

type Printer interface {
    Print(msg string)
}

type Console struct{}

func (Console) Print(msg string) { fmt.Println(msg) }

type TimestampWriter struct {
    Printer
}

func (t TimestampWriter) Print(msg string) {
    t.Printer.Print("[t] " + msg)
}

func main() {
    w := TimestampWriter{Printer: Console{}}
    w.Print("hello")
    w.Print("world")
}

Next: the handful of standard-library interfaces you'll actually implement.