Blog

Interfaces in Go: Implicit, Small and the nil Trap

A Go interface is a list of methods, and any type with those methods satisfies it without saying so. Learn method sets, Stringer, io.Reader, type switches, and why an error holding a nil pointer isn’t nil.

An interface in Go says what a value can do, not what it is. Any type with the right methods fits, and it never has to announce that it does. That one rule is why Go code tends to have small interfaces and few type hierarchies.

This post covers how types satisfy interfaces, the pointer-receiver rule that trips people up, the small interfaces in the standard library, any and type switches, and the nil trap that has bitten almost every Go programmer once. Every program below was run on Go 1.26, and its output is pasted from the run.

An interface is a set of methods

An interface type lists method signatures, and a value of any type that has all of those methods can be stored in it.

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) {
	fmt.Printf("%T: area %.2f, perimeter %.2f\n", s, s.Area(), s.Perimeter())
}

func main() {
	describe(Rect{W: 3, H: 4})
	describe(Circle{R: 1})

	shapes := []Shape{Rect{W: 2, H: 2}, Circle{R: 2}}
	total := 0.0
	for _, s := range shapes {
		total += s.Area()
	}
	fmt.Printf("total area %.2f\n", total)
}

It prints:

main.Rect: area 12.00, perimeter 14.00
main.Circle: area 3.14, perimeter 6.28
total area 16.57

Look at what’s missing. Rect and Circle never mention Shape. There’s no implements Shape anywhere. They have an Area method and a Perimeter method with the right signatures, and that’s enough.

describe accepts any Shape, and %T shows the concrete type that’s really inside. The slice []Shape holds a Rect and a Circle side by side, and the loop calls Area on each without knowing which is which.

Because satisfaction is implicit, you can define an interface after the types exist, even in a different package that the types’ authors have never heard of. The types don’t need to change.

The compiler checks where you use it

Implicit doesn’t mean unchecked. The moment you use a value as an interface, the compiler checks that its type has every method, and a missing one stops the build.

package main

import "fmt"

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

type Square struct {
	Side float64
}

func (s Square) Area() float64 { return s.Side * s.Side }

func describe(s Shape) {
	fmt.Println(s.Area(), s.Perimeter())
}

func main() {
	describe(Square{Side: 2})
}

The build fails with:

./main.go:21:11: cannot use Square{…} (value of struct type Square) as Shape value in argument to describe: Square does not implement Shape (missing method Perimeter)

The message names the missing method. There’s no runtime surprise here: a type that doesn’t fit is a compile error at the line that tried to use it.

Checking without a use site

Sometimes nothing in your package passes the type as the interface yet. A library type might only be used that way by callers. To get the check anyway, Go programmers write a line like this:

package main

import "fmt"

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

type Square struct {
	Side float64
}

func (s *Square) Area() float64 { return s.Side * s.Side }

var _ Shape = (*Square)(nil)

func main() {
	fmt.Println("never gets here")
}

The build fails with:

./main.go:16:15: cannot use (*Square)(nil) (value of type *Square) as Shape value in variable declaration: *Square does not implement Shape (missing method Perimeter)

var _ Shape = (*Square)(nil) declares a variable you can’t use, because its name is the blank identifier _. The value is a nil *Square, which costs nothing. The only point of the line is the assignment, which makes the compiler check that *Square satisfies Shape. Add Perimeter and the line compiles to nothing.

Pointer receivers and method sets

If a type’s methods have pointer receivers, only the pointer type satisfies the interface, and a plain value doesn’t.

package main

import "fmt"

type Counter interface {
	Inc()
	Value() int
}

type Clicks struct {
	n int
}

func (c *Clicks) Inc()       { c.n++ }
func (c *Clicks) Value() int { return c.n }

func main() {
	var c Counter = Clicks{}
	c.Inc()
	fmt.Println(c.Value())
}

The build fails with:

./main.go:18:18: cannot use Clicks{} (value of struct type Clicks) as Counter value in variable declaration: Clicks does not implement Counter (method Inc has pointer receiver)

This one surprises people, because on an ordinary variable you can call c.Inc() on a Clicks value and Go takes its address for you. That shortcut doesn’t apply to interfaces. Store a pointer instead:

package main

import "fmt"

type Counter interface {
	Inc()
	Value() int
}

type Clicks struct {
	n int
}

func (c *Clicks) Inc()       { c.n++ }
func (c *Clicks) Value() int { return c.n }

func main() {
	var c Counter = &Clicks{}
	c.Inc()
	c.Inc()
	fmt.Println(c.Value())
}

It prints:

2

The rule has a name: the method set. The method set of T holds the methods with value receivers. The method set of *T holds those plus the ones with pointer receivers. A type satisfies an interface when the interface’s methods are all in its method set.

The reason is what an interface stores. Putting Clicks{} in an interface copies the struct into it. If Go let Inc run on that copy, c.n++ would change a hidden copy, not anything you could see, and the count would silently stay at zero. Refusing to compile is the kinder answer. The part on structs and methods covers when to choose a pointer receiver in the first place.

Small interfaces from the standard library

The most useful interfaces in Go have one method, and the standard library is built on a handful of them.

fmt.Stringer

fmt.Stringer is declared as interface { String() string }. The fmt package checks for it, so any type with a String method controls how it prints.

package main

import "fmt"

type Temp float64

func (t Temp) String() string {
	return fmt.Sprintf("%.1f°C", float64(t))
}

type Point struct {
	X, Y int
}

func main() {
	t := Temp(21.456)
	fmt.Println(t)
	fmt.Printf("%v and %s\n", t, t)
	fmt.Println(Point{X: 1, Y: 2})
	fmt.Println(float64(t))
}

It prints:

21.5°C
21.5°C and 21.5°C
{1 2}
21.456

Temp has a String method, so Println, %v and %s all use it. Point has none, so it gets the default struct format. The last line is worth a second look. Converting t to float64 gives a value of a different type, and float64 has no String method, so the raw number comes back.

Inside String we call Sprintf with float64(t), not t, for the same reason. Passing t with %v would call String again, which would call Sprintf again, forever.

io.Reader and io.Writer

io.Reader has one method, Read(p []byte) (n int, err error), and io.Writer has one method, Write(p []byte) (n int, err error). Files, network connections, HTTP bodies, buffers and strings all satisfy one or both.

package main

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

type upperWriter struct {
	w io.Writer
}

func (u upperWriter) Write(p []byte) (int, error) {
	return u.w.Write(bytes.ToUpper(p))
}

func main() {
	r := strings.NewReader("hello from a string\n")
	n, err := io.Copy(os.Stdout, r)
	fmt.Println(n, err)

	loud := upperWriter{w: os.Stdout}
	io.Copy(loud, strings.NewReader("hello again\n"))
}

It prints:

hello from a string
20 <nil>
HELLO AGAIN

io.Copy takes a Writer and a Reader and moves bytes from one to the other until the reader runs out. It doesn’t know it’s reading a string or writing to the terminal. It returned 20, the number of bytes copied.

upperWriter is our own Writer. Its one method upper-cases the bytes and passes them to the writer it wraps. Four lines made a type that io.Copy, and every other function that takes a Writer, can use.

Accept interfaces, return structs

A common guideline in Go is that functions should accept interfaces and return concrete types. Accepting an interface lets the caller pass whatever they have.

package main

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

func countLines(r io.Reader) (int, error) {
	sc := bufio.NewScanner(r)
	n := 0
	for sc.Scan() {
		n++
	}
	return n, sc.Err()
}

func main() {
	n, err := countLines(strings.NewReader("one\ntwo\nthree\n"))
	fmt.Println(n, err)

	var buf bytes.Buffer
	buf.WriteString("alpha\nbeta\n")
	n, err = countLines(&buf)
	fmt.Println(n, err)
}

It prints:

3 <nil>
2 <nil>

countLines only needs Read, so it asks for an io.Reader. The same function counts lines in a string, a buffer, an open file or a request body, and tests can hand it a strings.Reader instead of a real file.

Returning a struct, such as *bytes.Buffer or *bufio.Scanner, goes the other way. The caller gets every method the type has, and can store it in whichever small interface they need. If you return an interface, you hide those methods and decide for the caller what they may do.

It’s a guideline, not a law. error is an interface, and functions return it all the time.

any, type assertions and comma-ok

any is another name for interface{}, the interface with no methods, so every type satisfies it. You met it in fmt.Println(a ...any). To get a concrete value back out, you use a type assertion.

package main

import "fmt"

func main() {
	var x any = "gopher"

	s, ok := x.(string)
	fmt.Printf("%q %v\n", s, ok)

	n, ok := x.(int)
	fmt.Println(n, ok)

	x = 42
	fmt.Println(x.(int) + 1)
	fmt.Println(x.(string))
}

It prints three lines, then stops:

"gopher" true
0 false
43
panic: interface conversion: interface {} is int, not string

x.(string) asks “is the value inside x a string?” With two results, s, ok := x.(string), you get the value and true when it is. When it isn’t, as in x.(int), you get the zero value and false, and nothing breaks. That’s the same comma-ok form maps use.

With one result there’s nowhere to report failure, so a wrong guess panics. The panic message still says interface {}, the older spelling, because any is only an alias. Use the one-result form only when a wrong type would be a bug in your own code.

Type switches, one level deeper

A type switch is a chain of type assertions, and its cases can name interfaces as well as concrete types. The part on control flow showed the basic shape. Here’s what else it can do.

package main

import (
	"errors"
	"fmt"
	"strconv"
)

type Celsius float64

func (c Celsius) String() string { return strconv.FormatFloat(float64(c), 'f', 1, 64) + "°C" }

func describe(x any) string {
	switch v := x.(type) {
	case nil:
		return "nil"
	case int, int64:
		return fmt.Sprintf("a whole number, %T %v", v, v)
	case error:
		return "an error: " + v.Error()
	case fmt.Stringer:
		return "a Stringer: " + v.String()
	default:
		return fmt.Sprintf("something else: %T", v)
	}
}

func main() {
	fmt.Println(describe(nil))
	fmt.Println(describe(7))
	fmt.Println(describe(int64(7)))
	fmt.Println(describe(Celsius(21.5)))
	fmt.Println(describe(errors.New("disk full")))
	fmt.Println(describe([]int{1, 2}))
}

It prints:

nil
a whole number, int 7
a whole number, int64 7
a Stringer: 21.5°C
an error: disk full
something else: []int

Three things are going on:

  • A case with one type gives v that type. In case error, v is an error, so v.Error() compiles.
  • A case with several types, like case int, int64, can’t pick one, so v stays any. You can print it, but you can’t do arithmetic on it without another assertion.
  • A case with an interface matches any value whose type has those methods. Celsius matched fmt.Stringer without ever naming it.

Cases are tried in order, and the first match wins. A type with both an Error and a String method would land in case error here, because it comes first.

Reach for a type switch when a value really can be one of several unrelated types, such as a decoded JSON value. When the types share behaviour, put that behaviour in an interface method and call it. That’s what Shape did at the start.

The nil trap

An interface holding a nil pointer is not itself nil, and that makes a function returning error report a failure that never happened.

package main

import "fmt"

type MyError struct {
	Msg string
}

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

func checkAge(age int) error {
	var e *MyError
	if age < 0 {
		e = &MyError{Msg: "age can't be negative"}
	}
	return e
}

func main() {
	err := checkAge(30)
	if err != nil {
		fmt.Println("failed:", err)
		return
	}
	fmt.Println("age is fine")
}

It prints:

failed: <nil>

The age was fine. e stayed a nil pointer. Yet err != nil was true, and the program took the failure branch.

The <nil> is a second surprise. fmt called Error on a nil *MyError, and e.Msg dereferenced nil. fmt recovers from that particular panic and prints <nil> instead. Call err.Error() yourself and the program crashes with a nil pointer dereference.

Explain it like I’m ten

Picture an interface as a box with a label on the lid. The box has two slots: the label, which says what kind of thing is inside, and the thing itself.

An empty box has nothing on the label and nothing inside. That’s the only box Go calls nil.

Now take a box, write “MyError” on the label, and put nothing inside. Is it empty? Someone checking “is the label blank and the box empty?” says no. The label is filled in. So err == nil is false, even though there’s nothing in it.

The precise version

An interface value is two words. The first says which concrete type is stored, and the second is the value of that type (for a pointer, the pointer itself). An interface is nil only when both words are empty.

return e converts the *MyError to an error. That conversion always records the type, *MyError, in the first word. The second word holds the nil pointer. One word is filled, so the interface isn’t nil.

Where the analogy breaks: a real box with nothing inside is useless, but an interface holding a nil pointer isn’t always a mistake. You can call methods on it, and a method written to handle a nil receiver works fine. The trap is only in comparing it with nil and expecting true.

Watching the two words

The two words of an interface value change at each step of the trap, and the animation below follows them:

type value err (error) nil nil p (*MyError) its type pointer *MyError nil *MyError nil err == nil is true p == nil is true err holds type *MyError, value nil err == nil is false err == nil is true again step 1: var err error leaves both slots empty step 2: var p *MyError = nil is a pointer to nothing step 3: err = p fills the type slot with *MyError; the value is nil step 4: the type slot is filled, so err is not nil the fix: return nil, and both slots stay empty

An interface value drawn as two cells, type and value. An empty err equals nil. Assigning a nil *MyError fills the type cell with *MyError while the value cell stays nil, so err no longer equals nil. Returning a plain nil leaves both cells empty.

Here are those steps in words, in case the animation doesn’t play for you:

  1. var err error makes an interface with both slots empty. err == nil is true.
  2. var p *MyError = nil makes a pointer that points at nothing. p == nil is true.
  3. err = p stores the type *MyError in the type slot and the nil pointer in the value slot.
  4. err == nil is now false, because the type slot isn’t empty.
  5. The fix: return nil instead of the pointer, and both slots stay empty.

This program walks through the first four steps:

package main

import "fmt"

type MyError struct {
	Msg string
}

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

func main() {
	var err error
	fmt.Println("step 1: err == nil is", err == nil)

	var p *MyError = nil
	fmt.Println("step 2: p == nil is", p == nil)

	err = p
	fmt.Printf("step 3: err holds type %T\n", err)

	fmt.Println("step 4: err == nil is", err == nil)
}

It prints:

step 1: err == nil is true
step 2: p == nil is true
step 3: err holds type *main.MyError
step 4: err == nil is false

p == nil compares a pointer with nil, and it’s true. err == nil compares an interface with the empty interface, and it’s false. The same nil on the right means two different things, depending on the type on the left.

The fix: return a plain nil

Don’t store an error in a variable of the concrete pointer type and return it. Return nil itself on success:

package main

import "fmt"

type MyError struct {
	Msg string
}

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

func checkAge(age int) error {
	if age < 0 {
		return &MyError{Msg: "age can't be negative"}
	}
	return nil
}

func main() {
	for _, age := range []int{30, -1} {
		if err := checkAge(age); err != nil {
			fmt.Println(age, "failed:", err)
			continue
		}
		fmt.Println(age, "is fine")
	}
}

It prints:

30 is fine
-1 failed: age can't be negative

return nil in a function whose result type is error produces an interface with both words empty. The rule to keep is simple: if a function returns error, its variables for errors should have type error too, never *MyError. The part on errors builds on this with wrapping, errors.Is and errors.As.

What to remember

  • An interface is a set of methods. A type satisfies it by having those methods, with no implements keyword.
  • The compiler checks at the point of use. var _ Shape = (*Square)(nil) forces the check when there’s no use site.
  • Pointer-receiver methods belong only to the pointer type’s method set, so store &T{} in the interface, not T{}.
  • Small interfaces do the most work: fmt.Stringer, io.Reader, io.Writer. Accept them as parameters, and return concrete types.
  • Use the comma-ok form, v, ok := x.(T), unless a wrong type really is a bug. A type switch case with several types leaves v as any.
  • An interface is nil only when its type and value are both empty. A nil *MyError returned as error is not nil, so return a plain nil.

An interface is nil only when it holds no type and no value.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.