Small Interfaces

Interfaces with one or two methods are easy to satisfy and easy to fake, which is why Go accepts interfaces and returns structs.

The fewer methods an interface lists, the more types can satisfy it. One method is the common size in Go. A single-method interface is trivial for a real type to fit and trivial to replace with a few lines of stand-in code in a test. A ten-method interface is neither, and callers usually need only one or two of those methods anyway.

Accept interfaces, return structs

Take an interface as a parameter, so a caller can pass anything that does the job. Return the concrete struct, or a pointer to it, so the caller keeps every method and field the type has. A constructor that returns an interface throws information away, and the caller has to use a type assertion to get it back.

Where the interface belongs

Declare the interface in the package that calls the methods, not in the package that defines the type. The calling package knows exactly which methods it uses, so the list stays short. The implementing package stays unaware, which is fine, because satisfaction is implicit.

Naming, longhand and shorthand

A one-method interface is usually named after the method plus "er": Read gives Reader. An interface can also be written inline in the parameter list. Both functions below take exactly the same thing.

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

func saveLong(w Writer) {}

// Shorthand: the same interface, with no name.
func saveShort(w interface{ Write(p []byte) (int, error) }) {}

Name it when two functions share it. Write it inline when exactly one signature needs it.

Example

Example · go
package main

import "fmt"

// One method is enough to be an interface, and easy to satisfy.
type Greeter interface {
	Greet(name string) string
}

// Formal is the concrete type.
type Formal struct {
	Title string
}

// NewFormal returns *Formal, not Greeter, so the caller keeps the
// Title field and any future methods.
func NewFormal(title string) *Formal {
	return &Formal{Title: title}
}

func (f *Formal) Greet(name string) string {
	return "Good evening, " + f.Title + " " + name
}

// greetAll accepts the interface, so a test can pass a stub.
func greetAll(g Greeter, names []string) {
	for _, n := range names {
		fmt.Println(g.Greet(n))
	}
}

func main() {
	greetAll(NewFormal("Dr"), []string{"Ada", "Alan"})
}

// Output:
// Good evening, Dr Ada
// Good evening, Dr Alan

When to use it

  • Shrink a function's dependency to the one method it actually calls so tests need a two-line stub.
  • Pass a closure where a one-method interface is expected, using a function type with that method.
  • Keep a constructor returning *Client so callers can still reach its other methods.

More examples

Splitting a wide interface into the parts callers use

The same concrete value is passed as two narrow interfaces, because it satisfies both. Split interfaces this way when a function's signature currently demands far more than the function body uses; the signature then documents the real dependency.

Example · go
package main

import "fmt"

// Wide interface. Every stand-in has to supply all four methods,
// even when the caller uses one.
type Store interface {
	Get(id int) (string, bool)
	Put(id int, v string)
	Delete(id int)
	Count() int
}

// Narrow interfaces, one method each.
type Getter interface {
	Get(id int) (string, bool)
}

type Counter interface {
	Count() int
}

// report needs two of the four methods, so it asks for those.
// Any Store can be passed to it.
func report(g Getter, c Counter, id int) string {
	v, ok := g.Get(id)
	if !ok {
		return fmt.Sprintf("id %d missing of %d", id, c.Count())
	}
	return fmt.Sprintf("%s (1 of %d)", v, c.Count())
}

type memStore map[int]string

func (m memStore) Get(id int) (string, bool) { v, ok := m[id]; return v, ok }
func (m memStore) Put(id int, v string)      { m[id] = v }
func (m memStore) Delete(id int)             { delete(m, id) }
func (m memStore) Count() int                { return len(m) }

func main() {
	s := memStore{1: "pen"}
	fmt.Println(report(s, s, 1))
	fmt.Println(report(s, s, 2))
}

// Output:
// pen (1 of 1)
// id 2 missing of 1

A plain function as an implementation

A one-method interface can be satisfied by a function type, which removes the need to declare a struct with no fields. The standard library uses this pattern for HTTP handlers, and it is the quickest way to supply a one-off implementation in a test.

Example · go
package main

import "fmt"

type Greeter interface {
	Greet(name string) string
}

// GreetFunc is a function type with a method. The method just
// calls the function value itself.
type GreetFunc func(name string) string

func (f GreetFunc) Greet(name string) string { return f(name) }

func main() {
	// No struct needed: convert a function and pass it as a Greeter.
	var g Greeter = GreetFunc(func(name string) string {
		return "hi " + name
	})
	fmt.Println(g.Greet("Ada"))

	// A named function works the same way.
	shout := func(name string) string { return "HI " + name }
	g = GreetFunc(shout)
	fmt.Println(g.Greet("Alan"))
}

// Output:
// hi Ada
// HI Alan

Interface in the caller, struct from the constructor

The constructor hands back *Catalogue so callers keep the concrete type, while total takes the interface so the stub fits without changing any production code. This is the standard arrangement for anything that talks to a database, a cache or a remote service.

Example · go
package main

import "fmt"

// Prices is declared where the methods are called, and lists only
// what this code uses.
type Prices interface {
	Price(sku string) (int, bool)
}

// Catalogue is the real implementation.
type Catalogue struct {
	prices map[string]int
}

func NewCatalogue(prices map[string]int) *Catalogue {
	return &Catalogue{prices: prices}
}

func (c *Catalogue) Price(sku string) (int, bool) {
	p, ok := c.prices[sku]
	return p, ok
}

// fakePrices is the whole stub a test needs: one type, one method.
type fakePrices map[string]int

func (f fakePrices) Price(sku string) (int, bool) {
	p, ok := f[sku]
	return p, ok
}

func total(p Prices, skus []string) int {
	sum := 0
	for _, sku := range skus {
		if price, ok := p.Price(sku); ok {
			sum += price
		}
	}
	return sum
}

func main() {
	skus := []string{"pen", "pad", "ghost"}
	fmt.Println(total(NewCatalogue(map[string]int{"pen": 150, "pad": 400}), skus))
	fmt.Println(total(fakePrices{"pen": 1, "pad": 2}, skus))
}

// Output:
// 550
// 3

Discussion

  • Be the first to comment on this lesson.