Interfaces, Type Switches and Generics

Article · 14:00

A Go interface is a set of method signatures, and a type satisfies it by having those methods: no implements keyword, and the implementation need not know the interface exists. Which is why the consumer declares it - plus the nil interface that is not nil.

Interfaces, Type Switches and Generics

A Go interface is a set of method signatures. A type satisfies it by having those methods - there is no implements keyword, and the implementation does not need to know the interface exists.

package main

import (
	"fmt"
	"strings"
)

// The interface is declared where it is USED, not next to the implementations.
type Shape interface {
	Area() float64
	Name() string
}

type Rectangle struct{ Width, Height float64 }
type Circle struct{ Radius float64 }

func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Name() string  { return "rectangle" }
func (c Circle) Area() float64    { return 3.141592653589793 * c.Radius * c.Radius }
func (c Circle) Name() string     { return "circle" }

func describe(shapes []Shape) string {
	var b strings.Builder
	for _, s := range shapes {
		fmt.Fprintf(&b, "%s: %.2f\n", s.Name(), s.Area())
	}
	return b.String()
}

func main() {
	fmt.Print(describe([]Shape{Rectangle{3, 4}, Circle{1}}))
}

Neither Rectangle nor Circle mentions Shape. That is what makes it possible to write an interface for a type you do not own - including one in the standard library.

Small interfaces, declared by the consumer

The convention is one or two methods. io.Reader and io.Writer have one each, and half the standard library composes out of them.

package main

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

// A function that takes io.Reader works with a file, a network connection, a
// string, or anything else anyone writes later.
func countLines(r io.Reader) (int, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return 0, err
	}
	return strings.Count(string(data), "\n"), nil
}

func main() {
	n, err := countLines(strings.NewReader("first\nsecond\nthird\n"))
	fmt.Println(n, err)

	// io.Writer, the other half. os.Stdout is one; so is strings.Builder.
	var b strings.Builder
	writeReport(&b, 3)
	writeReport(os.Stdout, 3)
	fmt.Print(b.String())
}

func writeReport(w io.Writer, count int) {
	fmt.Fprintf(w, "counted %d lines\n", count)
}

any, type assertions and type switches

any (an alias for interface{}) holds anything. Getting the concrete value back out is a type assertion, and the comma-ok form is the safe one.

package main

import "fmt"

func main() {
	values := []any{"hello", 42, 3.5, []int{1, 2}, nil, Point{1, 2}}

	for _, v := range values {
		fmt.Println(describe(v))
	}

	// A bare assertion panics when it is wrong; the comma-ok form does not.
	var value any = "text"
	s := value.(string)
	fmt.Println(s)

	if n, ok := value.(int); ok {
		fmt.Println("an int:", n)
	} else {
		fmt.Println("not an int, and no panic")
	}
}

type Point struct{ X, Y int }

func describe(v any) string {
	switch t := v.(type) {
	case nil:
		return "nothing at all"
	case string:
		return fmt.Sprintf("a string of %d: %s", len(t), t)
	case int:
		return fmt.Sprintf("an int, doubled: %d", t*2)
	case []int:
		return fmt.Sprintf("a slice of %d ints", len(t))
	case Point:
		return fmt.Sprintf("a point at %d,%d", t.X, t.Y)
	default:
		return fmt.Sprintf("something else: %T", t)
	}
}

The nil interface trap

An interface value has two parts: a type and a value. It is nil only when both are. A nil pointer stored in an interface is therefore not a nil interface - and this is the most notorious gotcha in the language.

package main

import "fmt"

type MyError struct{ msg string }

func (e *MyError) Error() string { return e.msg }

// Broken: returning a typed nil pointer as an error.
func broken() error {
	var e *MyError = nil
	return e                        // the interface holds (*MyError, nil)
}

// Correct: return the untyped nil.
func correct() error {
	return nil
}

func main() {
	if err := broken(); err != nil {
		fmt.Printf("broken() looks like a failure: %v (type %T)\n", err, err)
	}

	if err := correct(); err == nil {
		fmt.Println("correct() is properly nil")
	}

	fmt.Println("the rule: never declare a typed nil and return it as an interface")
}

Generics, since Go 1.18

package main

import (
	"fmt"
	"strings"
)

// A type parameter with a constraint. `comparable` is built in.
func Keys[K comparable, V any](m map[K]V) []K {
	keys := make([]K, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

// A union constraint: any of these underlying types.
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](values []T) T {
	var total T                    // the zero value of whatever T is
	for _, v := range values {
		total += v
	}
	return total
}

func Map[T, R any](values []T, f func(T) R) []R {
	out := make([]R, 0, len(values))
	for _, v := range values {
		out = append(out, f(v))
	}
	return out
}

func main() {
	fmt.Println(len(Keys(map[string]int{"a": 1, "b": 2})))
	fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{1.5, 2.5}))
	fmt.Println(Map([]int{1, 2, 3}, func(n int) string {
		return strings.Repeat("*", n)
	}))
}

The ~int means "any type whose underlying type is int", so a type Celsius int satisfies it too. Generics are worth reaching for when you would otherwise write the same function three times - and not much sooner: an interface is usually the more Go-like answer.

What to take away

  • Satisfaction is implicit: a type has the methods, so it satisfies the interface.
  • Declare interfaces where they are consumed, and keep them to one or two methods.
  • io.Reader/io.Writer are the model: small, composable, and everywhere.
  • Use the comma-ok form of a type assertion; a bare one panics.
  • An interface holding a typed nil pointer is not nil - never return one.
  • Generics for genuine repetition; comparable, any and ~T unions are the vocabulary.