Free Handbook · Every example compiled & verified

Interfaces

Write code that works with any type that has the right methods: implicit interfaces, Stringer, error, io.Reader and io.Writer, type switches and the nil-interface trap.

0 / 134 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Define small interfaces and let types satisfy them implicitly, without an implements keyword
  • Use the standard interfaces you meet daily: fmt.Stringer, error, io.Reader and io.Writer
  • Get the concrete value back out of an interface with comma-ok type assertions and type switches
  • Explain why an interface holding a nil pointer is not nil, and avoid returning one
  • Design by "accept interfaces, return structs", and sort with sort.Interface and slices.SortFunc
01

Interfaces are satisfied implicitly

An interface type is a set of method signatures: type Shape interface { Area() float64 }. Any type that has those methods automatically satisfies the interface — there is no implements keyword and no declaration linking the two. Rect never mentions Shape; it just happens to have an Area() float64 method.

A variable of interface type can hold any value whose type satisfies it. Calling a method on the interface runs the method of whatever concrete type is inside at that moment — dynamic dispatch. This lets you write one function, totalArea(shapes []Shape), that works for rectangles, circles and any shape somebody adds next year.

gomain.go
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Name() string
}

type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Name() string  { return "rect" }

type Circle struct{ R float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Name() string  { return "circle" }

func totalArea(shapes []Shape) float64 {
	total := 0.0
	for _, s := range shapes {
		fmt.Printf("%-6s %6.2f\n", s.Name(), s.Area()) // dispatch to the concrete type
		total += s.Area()
	}
	return total
}

func main() {
	shapes := []Shape{Rect{3, 4}, Circle{1}, Rect{W: 2, H: 2}}
	fmt.Printf("total  %6.2f\n", totalArea(shapes))

	var s Shape = Circle{2} // an interface variable
	fmt.Printf("%T %.2f\n", s, s.Area())
}
Outputcompiled & run with real Go
rect    12.00
circle   3.14
rect     4.00
total   19.14
main.Circle 12.57
Your turn

Add a Triangle{Base, Height float64} type with both methods and put one in shapes. You do not touch Shape or totalArea at all.

Error you will hit

Square does not implement Shape (missing method Name)

go
package main

import "fmt"

type Shape interface {
	Area() float64
	Name() string
}

type Square struct{ Side float64 }

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

func main() {
	var s Shape = Square{2}
	fmt.Println(s.Area())
}
# command-line-arguments
./main.go:15:16: cannot use Square{…} (value of struct type Square) as Shape value in variable declaration: Square does not implement Shape (missing method Name)
Why the compiler said that

Satisfaction is implicit but still checked at compile time. The moment you put a Square where a Shape is needed, the compiler checks the method set and names the first missing method.

The fix

Add the missing method with the exact signature. A common trick to get this check at the type declaration rather than at a distant call site is var _ Shape = Square{} at package level.

go
package main

import "fmt"

type Shape interface {
	Area() float64
	Name() string
}

type Square struct{ Side float64 }

func (s Square) Area() float64 { return s.Side * s.Side }
func (s Square) Name() string  { return "square" }

var _ Shape = Square{} // compile-time check

func main() {
	var s Shape = Square{2}
	fmt.Println(s.Area())
}
Error you will hit

Counter does not implement Incrementer (method Inc has pointer receiver)

go
package main

import "fmt"

type Incrementer interface {
	Inc()
}

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

func main() {
	c := Counter{}
	var i Incrementer = c
	i.Inc()
	fmt.Println(c.N)
}
# command-line-arguments
./main.go:15:22: cannot use c (variable of struct type Counter) as Incrementer value in variable declaration: Counter does not implement Incrementer (method Inc has pointer receiver)
Why the compiler said that

The method is declared on *Counter, so only a *Counter has it in its method set. A plain Counter stored in an interface is a copy the interface owns — calling a pointer method on it could never update c, so Go refuses rather than silently modifying a hidden copy.

The fix

Store a pointer in the interface.

go
package main

import "fmt"

type Incrementer interface {
	Inc()
}

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

func main() {
	c := Counter{}
	var i Incrementer = &c
	i.Inc()
	fmt.Println(c.N)
}
02

Small interfaces: Stringer, error, Reader, Writer

The most useful interfaces in Go have one method. "The bigger the interface, the weaker the abstraction." Four of them are everywhere in the standard library, and knowing them lets your types plug straight in.

InterfaceMethodWhat satisfying it gets you
fmt.StringerString() stringfmt.Println, %v and %s print your type the way you choose
errorError() stringyour type can be returned as an error
io.WriterWrite(p []byte) (n int, err error)works with fmt.Fprintf, files, HTTP responses, buffers, gzip, hashes
io.ReaderRead(p []byte) (n int, err error)works with bufio.Scanner, io.Copy, JSON decoders, files, network connections
gomain.go
package main

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

type Money struct {
	Cents int
}

func (m Money) String() string { // satisfies fmt.Stringer
	return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100)
}

type NotFoundError struct {
	ID string
}

func (e *NotFoundError) Error() string { // satisfies error
	return "item " + e.ID + " not found"
}

func find(id string) (Money, error) {
	if id != "a1" {
		return Money{}, &NotFoundError{ID: id}
	}
	return Money{Cents: 1999}, nil
}

func main() {
	price, _ := find("a1")
	fmt.Println("price:", price) // Println calls String()

	if _, err := find("zz"); err != nil {
		fmt.Println("error:", err) // Println calls Error()
	}

	var sb strings.Builder
	fmt.Fprintf(&sb, "written to a builder, ") // *strings.Builder is an io.Writer
	fmt.Fprintln(os.Stdout, sb.String()+"then to stdout")
}
Outputcompiled & run with real Go
price: $19.99
error: item zz not found
written to a builder, then to stdout
Your turn

Give Money a pointer receiver for String() instead. Does fmt.Println("price:", price) still use it? (Only a *Money has the method now.)

A function that accepts an io.Writer or io.Reader does not care where the bytes go or come from. The same report function below writes to the terminal in production and to a strings.Builder in a test; the same countLines reads a string today and a 10 GB file tomorrow.

gomain.go
package main

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

func report(w io.Writer, name string, score int) {
	fmt.Fprintf(w, "%s scored %d\n", name, score)
}

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

func main() {
	report(os.Stdout, "ana", 91) // to the terminal

	var buf strings.Builder
	report(&buf, "bo", 78) // to memory — how you would test it
	fmt.Printf("captured: %q\n", buf.String())

	input := strings.NewReader("one\ntwo\nthree\n")
	fmt.Println(countLines(input), "lines")
}
Outputcompiled & run with real Go
ana scored 91
captured: "bo scored 78\n"
3 lines
Your turn

Write func upper(r io.Reader, w io.Writer) that copies every line from r to w in upper case. Test it with strings.NewReader and os.Stdout.

03

The empty interface and type assertions

An interface with no methods, interface{}, is satisfied by every type. Go 1.18 added the alias any for it. fmt.Println(a ...any) takes any, and so does a JSON value decoded into map[string]any. The price: once a value is an any, the compiler no longer knows what it is, so you cannot call methods or do arithmetic on it until you get the concrete type back.

A type assertion does that: n := v.(int) says "I claim v holds an int". If it does not, the program panics. The comma-ok form n, ok := v.(int) never panics — ok is false and n is the zero value. Assertions also work to ask whether a value has more methods: s, ok := v.(fmt.Stringer).

gomain.go
package main

import "fmt"

type Temp float64

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

func main() {
	values := []any{42, "go", 3.5, Temp(21.5), nil}

	for _, v := range values {
		if n, ok := v.(int); ok {
			fmt.Println("int, doubled:", n*2)
		} else if s, ok := v.(fmt.Stringer); ok {
			fmt.Println("has String():", s.String())
		} else {
			fmt.Printf("something else: %v (%T)\n", v, v)
		}
	}
}
Outputcompiled & run with real Go
int, doubled: 84
something else: go (string)
something else: 3.5 (float64)
has String(): 21.5°C
something else: <nil> (<nil>)
Your turn

Add a branch for string that prints the string's length.

Error you will hit

panic: interface conversion: interface {} is string, not int

go
package main

import "fmt"

func main() {
	var v any = "42"
	n := v.(int)
	fmt.Println(n + 1)
}
panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
main.main()
	./main.go:7 +0x34
exit status 2
Why the compiler said that

The single-value assertion v.(int) is a promise; when the value inside is actually a string, Go panics and tells you both the real type and the one you asked for. This is the classic crash when handling decoded JSON, where every number arrives as float64, not int.

The fix

Use comma-ok and handle the mismatch — or better, keep real types and avoid any where you can.

go
package main

import "fmt"

func main() {
	var v any = "42"
	n, ok := v.(int)
	if !ok {
		fmt.Printf("expected int, got %T\n", v)
		return
	}
	fmt.Println(n + 1)
}
04

Type switches

When a value could be one of several types, a type switch is cleaner than a chain of assertions. switch x := v.(type) tests the dynamic type case by case, and inside each case x already has that case's type. A case can list several types (then x stays the interface type), and case nil matches an empty interface.

gomain.go
package main

import (
	"errors"
	"fmt"
)

func describe(v any) string {
	switch x := v.(type) {
	case nil:
		return "nil"
	case int:
		return fmt.Sprintf("int %d (next is %d)", x, x+1)
	case string:
		return fmt.Sprintf("string of %d bytes", len(x))
	case []int:
		return fmt.Sprintf("slice of %d ints", len(x))
	case error: // any type with Error() string
		return "error: " + x.Error()
	case float32, float64:
		return fmt.Sprintf("a float: %v", x) // x is still any here
	default:
		return fmt.Sprintf("unhandled %T", x)
	}
}

func main() {
	inputs := []any{7, "héllo", []int{1, 2}, errors.New("boom"), 2.5, nil, true}
	for _, v := range inputs {
		fmt.Println(describe(v))
	}
}
Outputcompiled & run with real Go
int 7 (next is 8)
string of 6 bytes
slice of 2 ints
error: boom
a float: 2.5
nil
unhandled bool
Your turn

Add a case for map[string]any that prints how many keys the map has. This is exactly how code walks a decoded JSON document.

A type switch is often a design smell
If you keep writing type switches over your own types, the behaviour probably belongs in a method on an interface instead — then adding a new type does not mean hunting down every switch. Type switches are the right tool at the edges: decoded JSON, any values from a library, and inspecting error types (Module 07 shows errors.As, the safer way to do that).
05

The nil interface gotcha

An interface value is a pair: a type and a value. It is nil only when both are unset. If you store a nil *NotFoundError in an error, the interface now holds (type = *NotFoundError, value = nil) — and that is not equal to nil. The if err != nil check passes, and a function that "returned no error" reports a failure.

gomain.go
package main

import "fmt"

type ValidationError struct{ Field string }

func (e *ValidationError) Error() string { return "invalid " + e.Field }

func validateBad(name string) error {
	var err *ValidationError // nil pointer
	if name == "" {
		err = &ValidationError{Field: "name"}
	}
	return err // wraps the (possibly nil) pointer in an error interface
}

func validateGood(name string) error {
	if name == "" {
		return &ValidationError{Field: "name"}
	}
	return nil // a truly nil interface
}

func main() {
	err := validateBad("ana")
	fmt.Println("bad:  err == nil?", err == nil)
	fmt.Printf("      inside: %T %v\n", err, err == (*ValidationError)(nil))

	err = validateGood("ana")
	fmt.Println("good: err == nil?", err == nil)
}
Outputcompiled & run with real Go
bad:  err == nil? false
      inside: *main.ValidationError true
good: err == nil? true
Your turn

Call err.Error() on the result of validateBad("ana"). Here it works because Error() never touches a field — change it to use e.Field and watch it panic.

VisualizeWhy the returned error is not nilStep 1 / 6
func validateBad(name string) error {
var err *ValidationError
if name == "" {
err = &ValidationError{Field: "name"}
}
return err
}
err := validateBad("ana")
fmt.Println(err == nil)
Line 9

Call validateBad("ana").

Variables now

nothing yet

All 6 steps as a table
StepLineWhat happenedVariables now
19Call validateBad("ana").
22err is a pointer variable, not an interface. It is nil.err (*ValidationError) = nil
33The name is not empty, so the pointer stays nil.err (*ValidationError) = nil
46The result type is error, so the pointer is wrapped in an interface. The interface records the type even though the value is nil.result (error) = (type=*ValidationError, value=nil)
59The caller's err is that interface.err (error) = (type=*ValidationError, value=nil)
610An interface equals nil only if type AND value are unset. The type is set, so the comparison is false.err (error) = (type=*ValidationError, value=nil)
The rule that prevents it
Never declare a variable of a concrete error type and return it as error. Return the literal nil on success, and declare error variables as var err error. The same trap applies to any interface — a nil *bytes.Buffer stored in an io.Writer is a non-nil writer that panics on first use.
06

Interface embedding and "accept interfaces, return structs"

Interfaces can embed other interfaces to build bigger ones from small pieces. The standard library does this constantly: io.ReadWriter is Reader + Writer, and io.ReadWriteCloser adds Closer. A type satisfies the combined interface when it has every method from all the pieces.

The design rule Go developers repeat is accept interfaces, return structs. A function accepts the smallest interface that covers what it uses, so callers can pass anything — including a fake in tests. A constructor returns the concrete type, so callers get every method and field without type assertions. And define the interface in the package that uses it, not the one that implements it: the consumer knows which methods it needs.

gomain.go
package main

import (
	"fmt"
	"strings"
)

// Small pieces...
type Sender interface {
	Send(to, msg string) error
}
type Counter interface {
	Sent() int
}

// ...embedded into a bigger interface.
type TrackedSender interface {
	Sender
	Counter
}

// A concrete type. The constructor returns *EmailSender, not an interface.
type EmailSender struct {
	log  strings.Builder
	sent int
}

func NewEmailSender() *EmailSender { return &EmailSender{} }

func (e *EmailSender) Send(to, msg string) error {
	e.sent++
	fmt.Fprintf(&e.log, "to=%s msg=%q; ", to, msg)
	return nil
}
func (e *EmailSender) Sent() int { return e.sent }

// The consumer accepts only what it needs.
func notifyAll(s Sender, users []string) {
	for _, u := range users {
		s.Send(u, "deploy finished")
	}
}

func main() {
	es := NewEmailSender()
	notifyAll(es, []string{"ana", "bo"})

	var t TrackedSender = es // *EmailSender has all three methods
	fmt.Println("sent:", t.Sent())
	fmt.Println(es.log.String())
}
Outputcompiled & run with real Go
sent: 2
to=ana msg="deploy finished"; to=bo msg="deploy finished";
Your turn

Write a fakeSender struct that records recipients in a []string, pass it to notifyAll, and print what it recorded. That is how you would unit-test notifyAll without sending email.

Do not create interfaces before you need them
Java habits lead to an interface for every struct (UserService + UserServiceImpl). In Go, start with the concrete type. Introduce an interface when a second implementation appears or a test needs a fake — implicit satisfaction means the existing type will fit it without any change.
07

Sorting: sort.Interface and slices.SortFunc

The sort package is a textbook interface design: sort.Sort accepts anything with Len(), Less(i, j int) bool and Swap(i, j int) — sort.Interface. You will still meet it in older code. Modern code (Go 1.21+) mostly uses slices.SortFunc with a comparison function that returns negative, zero or positive, built with cmp.Compare. slices.SortStableFunc keeps equal elements in their original order.

gomain.go
package main

import (
	"cmp"
	"fmt"
	"slices"
	"sort"
	"strings"
)

type Player struct {
	Name  string
	Score int
}

// byScore satisfies sort.Interface.
type byScore []Player

func (p byScore) Len() int           { return len(p) }
func (p byScore) Less(i, j int) bool { return p[i].Score < p[j].Score }
func (p byScore) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func main() {
	players := []Player{{"cy", 70}, {"ana", 90}, {"bo", 70}, {"dee", 85}}

	sort.Sort(byScore(players))
	fmt.Println(players)

	// Score descending, then name ascending for ties.
	slices.SortFunc(players, func(a, b Player) int {
		if c := cmp.Compare(b.Score, a.Score); c != 0 {
			return c
		}
		return strings.Compare(a.Name, b.Name)
	})
	fmt.Println(players)

	i, found := slices.BinarySearchFunc([]int{10, 20, 30}, 20, cmp.Compare[int])
	fmt.Println(i, found)
}
Outputcompiled & run with real Go
[{cy 70} {bo 70} {dee 85} {ana 90}]
[{ana 90} {dee 85} {bo 70} {cy 70}]
1 true

sort.Sort is not stable, so the order of the two 70-point players after the first sort is not guaranteed in general. The second sort breaks the tie by name, which makes the result fully determined.

Your turn

Sort the players by name length, shortest first, keeping the current order for equal lengths. Which function keeps that order?

Interface
A type defined by a set of method signatures. Any type with those methods satisfies it implicitly.
Method set
The methods a type has. T has its value-receiver methods; *T has both value- and pointer-receiver methods.
Dynamic dispatch
Calling a method through an interface runs the method of the concrete type stored in it at run time.
any
An alias for interface{}, the interface with no methods, satisfied by every type.
Type assertion
v.(T) extracts the concrete T from an interface; panics if wrong unless you use t, ok := v.(T).
Type switch
switch x := v.(type) — branches on the dynamic type of an interface value.
fmt.Stringer
The interface String() string; fmt uses it to print your type.
io.Writer / io.Reader
One-method interfaces for writing and reading bytes, satisfied by files, buffers, network connections, HTTP bodies and more.
Quick check

A function declared func find() error ends with var e *MyErr; return e. The caller writes if err := find(); err != nil. What happens?

Quick check

Counter has func (c *Counter) Inc(). Which of these can be assigned to a variable of type interface{ Inc() }?

Frequently asked questions

How does a Go type implement an interface?
By having all of the interface's methods with matching signatures. There is no implements keyword; the compiler checks the method set wherever the value is used as the interface. Add var _ MyInterface = (*MyType)(nil) to force that check at compile time.
What is the difference between any and interface{} in Go?
None. any is an alias for interface{} added in Go 1.18. Both mean an interface with no methods, which every type satisfies.
Why is my Go error not nil when the pointer is nil?
An interface value is a (type, value) pair and is nil only when both are empty. Returning a nil pointer of a concrete error type as an error produces an interface with a type set, so err != nil is true. Return a literal nil on success.

Finish the Go handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.