Skip to main content
Go · lesson 12 of 25
Go intermediate Lesson 12 of 25

Interfaces in Go

Go interfaces run for real: implicit satisfaction, pointer-receiver method sets, type assertions and switches, Stringer, io.Reader, and the nil trap.

By SkillByExample · Updated

An interface type lists methods. Any type with those methods satisfies it — no declaration needed. That one rule is what lets fmt, io and sort work with types they have never heard of, and it is behind one well-known bug, the non-nil nil error.

Implicit satisfaction

Circle and Rect never mention Shape, yet both can be used as one.

package main

import (
	"fmt"
	"math"
)

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

type Circle struct{ R float64 }
type Rect struct{ W, H 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 (r Rect) Area() float64        { return r.W * r.H }
func (r Rect) Perimeter() float64   { return 2 * (r.W + r.H) }

var _ Shape = Rect{} // compile-time check that Rect satisfies Shape

func main() {
	shapes := []Shape{Circle{1}, Rect{3, 4}}
	total := 0.0
	for _, s := range shapes {
		fmt.Printf("%-14T area %6.2f perimeter %6.2f\n", s, s.Area(), s.Perimeter())
		total += s.Area()
	}
	fmt.Printf("total area %.2f\n", total)
}
$ go run shapes.go
main.Circle    area   3.14 perimeter   6.28
main.Rect      area  12.00 perimeter  14.00
total area 15.14

The loop calls methods through the interface; Go picks Circle.Area or Rect.Area based on the concrete type stored in each element, which %T reveals. The var _ Shape = Rect{} line costs nothing at run time but makes the compiler prove Rect satisfies Shape, so a typo in a method name fails here rather than at some distant call site.

Pointer receivers and method sets

A method declared on *T is not part of T’s method set, so only the pointer satisfies the interface.

package main

import "fmt"

type Resetter interface{ Reset() }

type Counter struct{ n int }

func (c *Counter) Reset() { c.n = 0 }

func resetAll(rs ...Resetter) {
	for _, r := range rs {
		r.Reset()
	}
}

func main() {
	c := Counter{n: 5}
	resetAll(c)
	fmt.Println(c.n)
}
$ go run methodset.go
# command-line-arguments
./methodset.go:19:11: cannot use c (variable of struct type Counter) as Resetter value in argument to resetAll: Counter does not implement Resetter (method Reset has pointer receiver)

The compiler’s error names the cause: Reset has a pointer receiver, so only *Counter has it. The rule exists because a Counter copied into an interface couldn’t be modified by Reset in any meaningful way — the caller’s variable would never change. With resetAll(&c), the program prints 0.

Stringer: implementing a standard interface

fmt checks whether a value has a String() string method — the fmt.Stringer interface — and uses it.

package main

import "fmt"

type Celsius float64

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

type Status int

const (
	Active Status = iota
	Suspended
)

func (s Status) String() string {
	switch s {
	case Active:
		return "active"
	case Suspended:
		return "suspended"
	}
	return fmt.Sprintf("Status(%d)", int(s))
}

func main() {
	fmt.Println(Celsius(21.456), Suspended, Status(7))
	fmt.Printf("%v %d\n", Active, Active)
}
$ go run stringer.go
21.5°C suspended Status(7)
active 0

Neither type mentions fmt.Stringer, but Println and %v found the method. %d bypasses it and prints the number. Note the float64(c) conversion inside String: formatting c itself with %f is fine, but %v would call String again and recurse forever.

any, type assertions and type switches

any (an alias for interface{}) holds a value of any type. A type assertion gets the concrete value back.

package main

import "fmt"

func main() {
	var v any = "hello"

	s, ok := v.(string)
	fmt.Println(s, ok)

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

	fmt.Println(v.(int))
}
$ go run assert.go
hello true
0 false
panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
main.main()
	/home/user/gobyex/interfaces/assert/assert.go:14 +0xf0
exit status 2

The two-value form reports failure through ok and returns the zero value. The single-value form panics, and the message names both the type it found and the type you asked for. Use the panicking form only when a different type would be a programming error.

A type switch tests several types at once, and inside each case the variable has that case’s type:

package main

import "fmt"

func describe(v any) string {
	switch x := v.(type) {
	case nil:
		return "nil"
	case int, int64:
		return fmt.Sprintf("integer %v", x) // x is still any here
	case string:
		return fmt.Sprintf("string of %d bytes", len(x))
	case error:
		return "error: " + x.Error()
	case fmt.Stringer:
		return "stringer: " + x.String()
	default:
		return fmt.Sprintf("other %T", x)
	}
}

func main() {
	for _, v := range []any{nil, 42, int64(7), "héllo", fmt.Errorf("boom"), 3.5} {
		fmt.Println(describe(v))
	}
}
$ go run typeswitch.go
nil
integer 42
integer 7
string of 6 bytes
error: boom
other float64

Cases can name interfaces (error, fmt.Stringer) as well as concrete types, and the first match wins. In a case listing two types, x keeps the type any, because it could be either.

Small interfaces: io.Reader

The standard library’s most important interfaces have one method. Anything that implements Read works with every function that consumes an io.Reader.

package main

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

// upper wraps another Reader and upper-cases ASCII letters as they pass through.
type upper struct{ r io.Reader }

func (u upper) Read(p []byte) (int, error) {
	n, err := u.r.Read(p)
	for i := range n {
		if 'a' <= p[i] && p[i] <= 'z' {
			p[i] -= 'a' - 'A'
		}
	}
	return n, err
}

func main() {
	src := strings.NewReader("interfaces compose\n")
	data, err := io.ReadAll(upper{src})
	fmt.Print(string(data))
	fmt.Println(err)
}
$ go run reader.go
INTERFACES COMPOSE
<nil>

io.ReadAll knows nothing about upper; it only calls Read until it gets io.EOF, which ReadAll treats as success and turns into a nil error. The same wrapper would work on a file, an HTTP body or a gzip stream. Interfaces compose the same way: io.ReadWriter is just Reader and Writer embedded in one interface.

The nil interface trap

An interface value holds a (type, value) pair, and it equals nil only when both are nil.

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func validate(ok bool) error {
	var e *MyErr // nil pointer
	if !ok {
		e = &MyErr{}
	}
	return e // BUG: wraps a typed nil
}

func main() {
	err := validate(true)
	fmt.Println(err == nil)
	fmt.Printf("%T %v\n", err, err)
}
$ go run niltrap.go
false
*main.MyErr my error

Validation passed, yet err == nil is false. The function returned a nil *MyErr, and converting it to error produced an interface whose type is *MyErr — not empty, so not nil. Worse, %v called Error() on the nil pointer — which works, because the method never dereferences its receiver — and printed my error, so a log line would report a failure that never happened. The fix is to return a literal nil on success:

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func validate(ok bool) error {
	if !ok {
		return &MyErr{}
	}
	return nil
}

func main() {
	fmt.Println(validate(true) == nil, validate(false))
}
$ go run nilfix.go
true my error

Practice

1. Add a Triangle with sides A, B, C to shapes.go (use Heron’s formula for the area) and include Triangle{3, 4, 5} in the slice.

Solution
package main

import (
	"fmt"
	"math"
)

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

type Triangle struct{ A, B, C float64 }

func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func (t Triangle) Area() float64 {
	s := t.Perimeter() / 2
	return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}

func main() {
	var s Shape = Triangle{3, 4, 5}
	fmt.Printf("%.2f %.2f\n", s.Area(), s.Perimeter())
}
$ go run p1.go
6.00 12.00

Once both methods exist, Triangle is a Shape; nothing else had to change.

2. Give Rect a String() method so that fmt.Println(Rect{3, 4}) prints Rect 3x4.

Solution
package main

import "fmt"

type Rect struct{ W, H float64 }

func (r Rect) String() string { return fmt.Sprintf("Rect %gx%g", r.W, r.H) }

func main() {
	fmt.Println(Rect{3, 4}, []Rect{{1, 2}, {5, 6}})
}
$ go run p2.go
Rect 3x4 [Rect 1x2 Rect 5x6]

fmt uses the method for elements inside a slice too. %g drops the trailing zeros that %f would print.

3. Write sumNumbers(vals []any) float64 that adds every int and float64 in the slice and ignores everything else.

Solution
package main

import "fmt"

func sumNumbers(vals []any) float64 {
	total := 0.0
	for _, v := range vals {
		switch x := v.(type) {
		case int:
			total += float64(x)
		case float64:
			total += x
		}
	}
	return total
}

func main() {
	fmt.Println(sumNumbers([]any{1, 2.5, "3", nil, 4}))
}
$ go run p3.go
7.5

Inside each single-type case x has that concrete type, so float64(x) and total += x compile. The string "3" is skipped, not parsed.

4. Write a countingWriter that implements io.Writer, counts bytes written, and use it with fmt.Fprintf.

Solution
package main

import (
	"fmt"
	"os"
)

type countingWriter struct{ n int }

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

func main() {
	cw := &countingWriter{}
	fmt.Fprintf(cw, "hello, %s\n", "interfaces")
	fmt.Println(cw.n, "bytes")
}
$ go run p4.go
hello, interfaces
18 bytes

Write has a pointer receiver because it updates n, so &countingWriter{} is the value that satisfies io.Writer.

Frequently Asked Questions

How does a type implement an interface in Go?
Implicitly: a type satisfies an interface as soon as it has every method the interface lists, with matching signatures. There is no implements keyword. A compile-time check such as var _ Shape = Circle{} makes the compiler confirm it.
Why doesn't my struct satisfy an interface when its method has a pointer receiver?
Methods with pointer receivers belong to the method set of *T, not T. So if Save is declared on *File, a *File satisfies the interface but a File value does not. Pass &f, or declare the method on the value receiver if it does not modify the struct.
What is the empty interface in Go?
interface{}, spelled any since Go 1.18, has no methods, so every type satisfies it. It is used where a value of any type is acceptable, such as fmt.Println's arguments. Getting a concrete value back requires a type assertion or type switch.
What is a type assertion?
x.(T) extracts the concrete value of type T from interface value x. The single-result form panics if x does not hold a T; the two-result form v, ok := x.(T) returns ok == false instead. A type switch tests several types at once.
Why is my error not nil when I returned a nil pointer?
An interface value is nil only when both its type and value are nil. Returning a nil *MyError as an error produces an interface whose type is *MyError and whose value is nil, which is not equal to nil. Return a literal nil for the no-error case.