Skip to content
AgileLogics
7 min read

Interfaces

Prerequisites: Pointer receivers & method sets

An interface is a set of method signatures. Any type that has those methods satisfies the interface automatically — there is no implements keyword. This lets you write functions that accept "anything that can do X".

Go code favours small interfaces, often with a single method, such as fmt.Stringer (String() string), io.Reader (Read([]byte) (int, error)) and error (Error() string).

Syntax

type Shape interface {
    Area() float64
}

var s Shape = Rect{W: 3, H: 4} // Rect satisfies Shape because it has Area()

r := s.(Rect)       // type assertion: panics if s does not hold a Rect
r, ok := s.(Rect)   // "comma ok" form: ok is false instead of panicking

switch v := s.(type) { // type switch
case Rect:
    // v is a Rect here
case Circle:
    // v is a Circle here
}

The empty interface interface{} — spelled any since Go 1.18 — has no methods, so every type satisfies it. Use it sparingly: you lose compile-time type checking and have to assert the type back out.

Example

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Rect struct{ W, H float64 }
type Circle struct{ R float64 }

func (r Rect) Area() float64   { return r.W * r.H }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

// String makes Rect a fmt.Stringer, so fmt uses it when printing.
func (r Rect) String() string { return fmt.Sprintf("Rect(%gx%g)", r.W, r.H) }

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int %d", x)
    case string:
        return fmt.Sprintf("string of length %d", len(x))
    case Shape:
        return fmt.Sprintf("shape with area %.2f", x.Area())
    default:
        return fmt.Sprintf("something else: %T", x)
    }
}

func main() {
    shapes := []Shape{Rect{W: 3, H: 4}, Circle{R: 1}}
    for _, s := range shapes {
        fmt.Printf("%v -> %.2f\n", s, s.Area())
    }

    var s Shape = Circle{R: 2}
    if c, ok := s.(Circle); ok {
        fmt.Println("radius:", c.R)
    }
    if _, ok := s.(Rect); !ok {
        fmt.Println("not a Rect")
    }

    fmt.Println(describe(42))
    fmt.Println(describe("gopher"))
    fmt.Println(describe(Rect{W: 2, H: 5}))
    fmt.Println(describe(3.5))
}

Expected output

Rect(3x4) -> 12.00
{1} -> 3.14
radius: 2
not a Rect
int 42
string of length 6
shape with area 10.00
something else: float64

Circle has no String method, so %v prints its fields as {1}.

Common mistakes

  • Defining big interfaces up front "just in case". Define interfaces where they are used, with only the methods that code needs.
  • Using the single-value assertion s.(Rect) when you aren't sure of the type — it panics on a mismatch. Use the v, ok := form.
  • Assuming an interface holding a nil pointer is nil. An interface is only nil when it has no type and no value; var p *Rect; var s Shape = p gives s != nil.
  • Reaching for any when a concrete type or a small interface would do.

Practice

Define an interface Animal with a method Sound() string. Create Dog and Cat types that satisfy it, put one of each in a []Animal, and print each sound.

Show solution
package main

import "fmt"

type Animal interface {
    Sound() string
}

type Dog struct{}
type Cat struct{}

func (Dog) Sound() string { return "Woof" }
func (Cat) Sound() string { return "Meow" }

func main() {
    animals := []Animal{Dog{}, Cat{}}
    for _, a := range animals {
        fmt.Println(a.Sound())
    }
}