CHAPTER 21 · TYPES & ABSTRACTIONS

Interfaces

Interfaces are the single most powerful abstraction in Go. They specify behavior (a set of method signatures a type must provide), and Go's implicit implementation rule makes them effortless to use. If the method set fits, the type "is" the interface. No implements keyword, no rigid hierarchy.

Learning objectives

  • Define an interface and understand method sets.
  • Explain what "implicit implementation" means and why it matters.
  • Use any (empty interface) and know when to avoid it.
  • Extract concrete types with type assertions and type switches.
  • Implement Stringer to customize fmt output.
  • Recognize the "typed nil interface" gotcha.

Defining an interface

type Speaker interface {
    Speak() string
}

That's it. An interface is a named method set with zero or more signatures. No fields, no implementations.

Implicit implementation

A type satisfies an interface simply by having the right methods. You don't declare it; Go checks at compile time:

type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof, I'm " + d.Name }

var s Speaker = Dog{Name: "Buddy"}    // Dog satisfies Speaker automatically
fmt.Println(s.Speak())

This is called duck typing, verified by the compiler: if it quacks like a Speaker (has a Speak() string method), it is one. Two huge benefits:

  1. You can retroactively make existing types satisfy new interfaces.
  2. Packages don't need to know about interfaces their consumers might define. Define interfaces where you use them, not where the type is defined.

Polymorphism through interfaces

type Shape interface { Area() float64 }

type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

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

func totalArea(shapes []Shape) float64 {
    var sum float64
    for _, s := range shapes {
        sum += s.Area()     // dispatches to the concrete type's method
    }
    return sum
}

shapes := []Shape{Circle{R: 1}, Square{Side: 2}}
fmt.Println(totalArea(shapes))

Empty interface: any

An interface with zero methods is satisfied by every type:

var x any = 42
x = "hello"
x = []int{1, 2, 3}

As of Go 1.18, any is an alias for interface{}. Use any in new code; it reads better.

!
any is a last resort Once a value is any, you've opted out of static typing. You need a type assertion or type switch to do anything useful with it. Prefer a more specific interface whenever possible.

Type assertions

var i any = "hello"

s := i.(string)               // "hello": panics if i isn't actually a string

s, ok := i.(string)           // safe form: ok is false if wrong type
if ok {
    fmt.Println(s)
}

Use the comma-ok form unless you're 100% sure of the type.

Type switches

func describe(v any) {
    switch x := v.(type) {
    case int:      fmt.Println("int:", x)
    case string:   fmt.Printf("string: %q\n", x)
    case []int:    fmt.Println("int slice, len", len(x))
    case nil:      fmt.Println("nil")
    default:       fmt.Printf("unknown %T\n", x)
    }
}

Cleaner than a chain of type-assertion ifs when you need to branch on multiple possible types.

The Stringer idiom

The most common tiny interface in Go:

type Stringer interface {
    String() string
}

If your type implements String() string, the fmt package automatically uses it:

type Weekday int
const (
    Sun Weekday = iota
    Mon
    Tue
    // ...
)

func (d Weekday) String() string {
    return []string{"Sun", "Mon", "Tue"}[d]
}

fmt.Println(Mon)     // Mon: not "1"

Also handy: error is literally an interface, type error interface { Error() string }. Chapter 23 covers that.

The typed-nil-interface gotcha

An interface value has two components: (concrete type, value). It's nil only when both are nil:

var p *User          // nil pointer
var i any = p        // interface holding (type=*User, value=nil)

fmt.Println(p == nil)    // true
fmt.Println(i == nil)    // false: interface is NOT nil

This trips up nearly every Go programmer at least once. If you return an interface from a function, return the literal nil (not a typed-nil pointer wrapped in the interface).

Interface design

  • Keep interfaces small. io.Reader has one method. That's why it's everywhere.
  • Accept interfaces, return structs. Be flexible about inputs; be concrete about outputs.
  • Define them where they're used, not where implementing types live. This keeps packages decoupled.

Check your understanding

Practice exercises

EXERCISE 1

Build a Shape interface

Define a Shape interface with Area() float64 and Perimeter() float64. Implement it for Circle, Rectangle, and Triangle. Write a function describe(s Shape) that prints the area and perimeter.

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

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

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

func describe(s Shape) {
    fmt.Printf("%T: area=%.2f perimeter=%.2f\n", s, s.Area(), s.Perimeter())
}

Notice: nothing declares that Circle "implements Shape". The compiler just checks method sets.

Further reading

Interfaces, Go's superpower.