Gofinity
GitHub
lesson 12 / 22~16m read

Interfaces

A type satisfies an interface by having its methods, and nothing else: no declaration, no registration, and one famous trap where a nil pointer is not a nil interface.

Interfaces

Methods attached behaviour to one type. Interfaces are how code stops caring which type it got. Go's version is unusual in one respect, and everything worth knowing follows from it: satisfaction is implicit.

No implements keyword

An interface is a list of method signatures:

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

A type satisfies it by having those methods. Rect does not mention Shape, Shape does not mention Rect, and there is no registration step. The compiler checks the match at the point of use - an assignment, an argument, an append to a []Shape - and that is the only place the relationship exists at all.

The consequence is that the interface belongs to the consumer. In a language with implements, the author of a type decides which interfaces it fits, and must have thought of yours. In Go you write the interface in the package that needs it, listing only the methods you actually call, and every type that already has them fits. That includes types from libraries written before your interface existed.

So the Go idiom runs backwards from what other languages teach: do not start with an interface and write types to fit it. Write the concrete types, and introduce an interface when a second implementation, or a test double, gives you something to abstract over. "Accept interfaces, return structs" is the short form - a function's parameters say what it needs, and its return type stays concrete so callers keep every method.

Small is the point

The interfaces that carry the standard library are one method:

type Stringer interface{ String() string }
type Reader   interface{ Read(p []byte) (n int, err error) }
type Writer   interface{ Write(p []byte) (n int, err error) }
type error    interface{ Error() string }

io.Reader is satisfied by files, network connections, strings.Reader, bytes.Buffer, gzip streams, HTTP bodies, and anything you write this afternoon. That reach comes from having exactly one method. A four-method interface is satisfied by whatever you designed for it and nothing else.

Larger interfaces get built by embedding rather than by growing:

type ReadWriter interface {
	Reader
	Writer
}

What an interface value is

An interface value is a pair: a type and a value of that type.

var s Shape = Rect{W: 2, H: 3}   // type Rect, value {2 3}

The call s.Area() looks up Rect's Area and runs it, so the same line does different work depending on what is inside. The zero value of an interface is nil: no type, no value. Calling a method on it panics.

Two interface values are equal when both halves are equal - and comparing them panics if the concrete type is not comparable, which is how a == between two interfaces holding slices blows up at run time rather than at compile time.

Getting the concrete value back

A type assertion, in the comma-ok form that asks instead of insisting:

r, ok := s.(Rect)      // ok is false when s holds something else

Without , ok a failed assertion panics. The same syntax with an interface on the right asks whether the value has that method set as well, which is how you probe for an optional capability:

if n, ok := s.(Named); ok {
	fmt.Println(n.Name())
}

For several possibilities, a type switch:

switch v := v.(type) {
case nil:
	// the interface held nothing
case int:
	// v is an int
case fmt.Stringer:
	// anything with a String method
default:
	// v is still the interface
}

Two rules earn their keep here. Order matters: an interface case matches everything satisfying it, so concrete cases go first. And a named type is its own type: type Celsius float64 matches case Celsius, never case float64. That is the same no-implicit-conversion rule you met with numbers, showing up in a new place.

When you find yourself type-switching over your own types, that is usually a method wanting to be written. A switch over types from elsewhere - any out of a JSON decode, an error from a library - is fine and normal.

Method sets, again

The rule from the last lesson is what you will actually hit:

  • the method set of *T holds every method
  • the method set of T holds only the value-receiver ones
type Adder interface{ Add(int) }

var _ Adder = (*Counter)(nil)   // fine
var _ Adder = Counter{}         // does not compile

The error message - "does not implement Adder (method Add has pointer receiver)" - is worth recognising on sight. The fix is almost always &, not a change of receiver.

var _ Shape = Rect{} at package level is the standard way to assert satisfaction where you define the type, so the failure lands in the file that caused it rather than in some distant caller. It costs nothing at run time.

The empty interface

interface{} has no methods, so everything satisfies it. any is an alias for it, added in Go 1.18, and is what you should write.

any says nothing, and that is its problem: getting a usable value back out takes an assertion or a type switch, checked at run time. It is right for genuinely heterogeneous data - fmt.Println's arguments, a decoded JSON document, a cache of unrelated things - and wrong as a way to avoid deciding on a type. Since generics, most of the old any signatures have a better version with a type parameter, which keeps the checking at compile time. Generics are two lessons away.

The nil interface trap

The one Go surprise that everybody meets. An interface is nil only when both halves are nil. A nil pointer stored in an interface is a non-nil interface:

var f *Failure         // f == nil
var err error = f      // err != nil

In a return statement it becomes a bug that fires on every call:

func Do() error {
	var f *Failure
	if wentWrong() {
		f = &Failure{...}
	}
	return f          // never nil as an error
}

Every caller's if err != nil is true, always. And it is quiet, because a method on a nil pointer runs perfectly well - only a dereference panics - so nothing crashes to tell you.

Three habits keep it away:

  1. Return a bare nil, never a typed nil variable.
  2. Guard at the boundary where a concrete pointer becomes an interface: if f == nil { return nil }.
  3. Do not declare interface-typed variables to hold concrete pointers on their way somewhere.

To detect one after the fact, assert back to the pointer and compare: f, ok := err.(*Failure); ok && f == nil.

Further reading

Practise

Three challenges. The first is satisfaction itself: two shapes, one interface neither of them names, a slice holding both, and a probe for a second interface with s.(Named). The second is the type switch - eight kinds of value out of one any, with a fmt.Stringer case that has to sit after the concrete ones and a named float64 that proves why. The third is the nil interface trap, written both ways: the broken function you can run, the fixed one beside it, and the guards that keep a nil pointer from ever becoming a non-nil error.

practise

Practise what you learned

Work through these in order. Each one asks a little more than the last.

Start The Shape Interface
  1. The Shape InterfaceEasy~20m+50 XP
  2. Type SwitchMedium~30m+100 XP
  3. The Nil Interface TrapHard~35m+150 XP