Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9. Interfaces and Generics

An interface is a set of method signatures. A type satisfies it by having those methods, and says nothing about it. That one decision — satisfaction is structural, checked by the compiler, never declared — is what makes Go’s abstraction feel different from every language with an implements keyword.

9.1 Interfaces

// A type satisfies an interface by having its methods. Nothing is declared.
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Rect struct{ W, H float64 }

func (r Rect) Area() float64      { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

type Circle struct{ R float64 }

func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }

func describe(s Shape) string {
	return fmt.Sprintf("%T area=%.2f perimeter=%.2f", s, s.Area(), s.Perimeter())
}

func main() {
	for _, s := range []Shape{Rect{3, 4}, Circle{1}} {
		fmt.Println(describe(s))
	}
}
main.Rect area=12.00 perimeter=14.00
main.Circle area=3.14 perimeter=6.28

Rect and Circle never mention Shape. They have the two methods, so they are Shapes, and a third type in another package written by somebody who never heard of yours is a Shape too if it has them. Interfaces can therefore be defined after the types that satisfy them, and by the package that needs them rather than the package that provides them.

That leads to the piece of Go design advice worth internalizing early: define an interface where it is used, not where it is implemented, and keep it small. A function that only reads should take an io.Reader, not your *FileStore, and the interface should list the one method the function calls. Then any caller can pass anything, including a strings.Reader in a test, without either side having planned for it.

The standard library’s most-used interfaces have one method each:

type Reader interface{ Read(p []byte) (n int, err error) }
type Writer interface{ Write(p []byte) (n int, err error) }
type Stringer interface{ String() string }
// One interface, one method, and a type of your own that satisfies it.
package main

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

// report writes to anything with a Write method.
func report(w io.Writer, name string) {
	fmt.Fprintf(w, "report for %s\n", name)
}

// countingWriter counts the bytes passing through it and forwards them.
type countingWriter struct {
	inner io.Writer
	n     int
}

func (c *countingWriter) Write(p []byte) (int, error) {
	c.n += len(p)
	return c.inner.Write(p)
}

func main() {
	var buf bytes.Buffer
	var sb strings.Builder

	report(&buf, "a byte buffer")
	report(&sb, "a string builder")
	report(os.Stdout, "standard output")

	fmt.Print(buf.String(), sb.String())

	// A wrapper is a writer too, so it drops in anywhere one is expected.
	c := &countingWriter{inner: os.Stdout}
	report(c, "a counting wrapper")
	fmt.Println("bytes written:", c.n)
}
report for standard output
report for a byte buffer
report for a string builder
report for a counting wrapper
bytes written: 30

Files, network connections, HTTP response bodies, gzip compressors, hash functions, and strings.Builder are all io.Writers, so anything written against that one method works with all of them. countingWriter shows the other half: because the interface is small, wrapping one is a few lines, and the wrapper is itself a writer.

Interfaces embed: io.ReadWriter is declared as interface { Reader; Writer } and nothing more.

9.2 Interface Values

An interface value holds a dynamic type and a value. %T prints the first, so the shapes above reported main.Rect. An interface is nil only when both halves are empty, the bug in section 8.4.

A type assertion extracts the concrete value back out. s.(Circle) panics if the dynamic type is not Circle; c, ok := s.(Circle) reports it instead and is what you should write. Asserting to an interface type instead asks whether the value also satisfies that one. That is how code discovers optional behavior: if wt, ok := w.(io.WriterTo); ok takes a fast path when the writer has one.

A type switch handles several possibilities at once, and it is where Go sends you when you want a sum type.

9.3 Rebuilding a Sum Type

Lux gives you an enum with associated values and a match the compiler forces you to complete. Go has neither. What you build instead is an interface with an unexported method, which no package but yours can implement, and a type switch.

// What a Lux enum becomes in Go, and what you give up.
package main

import "fmt"

// Shape is closed to other packages: the unexported method cannot be
// implemented outside this one.
type Shape interface {
	isShape()
}

type Circle struct{ R float64 }
type Square struct{ Side float64 }
type Tri struct{ A, B, C float64 }

func (Circle) isShape() {}
func (Square) isShape() {}
func (Tri) isShape()    {}

func name(s Shape) string {
	switch v := s.(type) {
	case Circle:
		return fmt.Sprintf("circle r=%g", v.R)
	case Square:
		return fmt.Sprintf("square s=%g", v.Side)
	default:
		// Nothing made us handle Tri. This is the case Lux would have caught.
		return fmt.Sprintf("unhandled %T", v)
	}
}

func main() {
	for _, s := range []Shape{Circle{1}, Square{2}, Tri{3, 4, 5}} {
		fmt.Println(name(s))
	}

	// A type assertion asks for one specific type.
	var s Shape = Circle{7}
	if c, ok := s.(Circle); ok {
		fmt.Println("radius", c.R)
	}
	if _, ok := s.(Square); !ok {
		fmt.Println("not a square")
	}
}
circle r=1
square s=2
unhandled main.Tri
radius 7
not a square

isShape() is a marker. It does nothing, and because it is unexported no type outside this package can have it, so the set of Shapes is closed and a reader can find all of them by searching one file. That much translates.

What does not translate is the guarantee. name handles Circle and Square and silently does the wrong thing for Tri, and the compiler is content: default covers it. Lux would have refused to build. This is the concrete cost of the different bet described in the preface, and the mitigations are habits rather than checks. Put the type switch and the type declarations in the same file. Make default panic with the unhandled type during development rather than returning something plausible. And when the set is large or changing, add a linter that checks type switches for completeness, since the compiler will not.

9.4 any

any is an alias for interface{}, the interface with no methods, which every type satisfies. It is the parameter type of fmt.Println and the value type of a heterogeneous map.

Reach for it rarely. A value in an any has had its type erased as far as the compiler is concerned, and getting it back means an assertion that can fail at run time, so a function taking any has given up the checking you came to Go for. Before generics it was the only tool for writing something that worked on many types. Now it usually is not.

9.5 Generics

A function can take type parameters, written in square brackets before the value parameters.

// Type parameters: one function, many types.
package main

import (
	"fmt"
	"strings"
)

// Map applies f to every element, returning a new slice.
func Map[T, U any](xs []T, f func(T) U) []U {
	out := make([]U, 0, len(xs))
	for _, x := range xs {
		out = append(out, f(x))
	}
	return out
}

// Number constrains T to the types that + works on.
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](xs []T) T {
	var total T
	for _, x := range xs {
		total += x
	}
	return total
}

type Celsius float64 // ~float64 admits named types too

func main() {
	fmt.Println(Map([]int{1, 2, 3}, func(n int) string {
		return strings.Repeat("*", n)
	}))
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2.5}))
	fmt.Println(Sum([]Celsius{10, 20}))
}
[* ** ***]
6
4
30

Map[T, U any] works on a slice of any type and produces a slice of any other. The call sites pass no type arguments because the compiler infers them from the values, as usual.

A constraint is an interface used as a bound on a type parameter. any allows everything and therefore permits almost nothing, so Sum cannot use it: + is not defined on a type parameter bounded by any. Number is a constraint listing the permitted underlying types, and the ~ means “any type whose underlying type is this”, so Celsius is admitted. comparable is built in and allows ==, as a map key parameter needs. cmp.Ordered in the standard library covers every type that < works on, so most constraints you need are already written.

When to use generics. When you would otherwise write the same function once per type, as the slices and maps packages do. Not when an interface with a method would do the job — a generic function knows nothing about T beyond its constraint, so if the behavior differs per type you want a method, not a type parameter. The Go proverb is that a little duplication is cheaper than the wrong abstraction, and generics have not repealed it.

Methods cannot have their own type parameters. A type can be generic, so type Stack[T any] struct { items []T } works and its methods use the type’s parameters, but you cannot add a parameter on the method itself.

Exercise 9-1. Write a Shape implementation in a second file for a right triangle and confirm you did not have to modify anything.

Exercise 9-2. Write an io.Writer that discards everything and counts calls. Compare it against io.Discard.

Exercise 9-3. Add Tri to the type switch in sumtype, then add a fourth shape and do not add it to the switch. Note that nothing tells you.

Exercise 9-4. Write func Filter[T any](xs []T, keep func(T) bool) []T and func Keys[K comparable, V any](m map[K]V) []K. Then find the standard-library functions that already do both.