28. What an interface is

📖 Reading · 10 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

An interface is a list of method names. Any type that has those methods satisfies the interface — automatically, with no declaration anywhere. That one idea is the centre of Go's design.

Declaring one

package main

import "fmt"

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

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

func main() {
    var s Shape = Rectangle{Width: 3, Height: 4}

    fmt.Println(s.Area())
    fmt.Println(s.Perimeter())
}

Shape says: anything with an Area() float64 and a Perimeter() float64 is a Shape. Rectangle has both, so it is one.

Look at Rectangle again: it never mentions Shape. No implements, no : Shape, no registration. You could delete the Shape interface entirely and Rectangle wouldn't change. This is structural typing, and Go programmers call it implicit satisfaction.

Why implicit matters

In Java or C#, a type must be declared as implementing an interface — so the interface has to exist before the type, and it usually lives with the implementation. In Go the interface can be written afterwards, by somebody else, in a different package.

That inverts who owns the abstraction. The consumer defines the interface, listing only the methods it actually needs, and every existing type that happens to have them fits. You can write an interface today that time.Time or a third-party library type already satisfies.

One interface, several types

package main

import (
    "fmt"
    "math"
)

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

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }

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

func main() {
    shapes := []Shape{
        Rectangle{Width: 3, Height: 4},
        Circle{Radius: 5},
        Rectangle{Width: 1, Height: 1},
    }

    total := 0.0
    for _, s := range shapes {
        describe(s)
        total += s.Area()
    }
    fmt.Printf("total area %.2f\n", total)
}

describe works on anything shaped like a Shape. A []Shape can hold a Rectangle and a Circle side by side, because what's stored is the interface value, not the concrete type.

This is Go's polymorphism. No base class, no hierarchy — just "does it have the methods".

Interface values hold two things

An interface value is a pair: the concrete type and the value.

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{ Name string }
type Robot struct{ ID int }

func (d Dog) Speak() string   { return d.Name + " says woof" }
func (r Robot) Speak() string { return fmt.Sprintf("unit %d reporting", r.ID) }

func main() {
    var s Speaker

    fmt.Printf("%v %T\n", s, s)

    s = Dog{Name: "Rex"}
    fmt.Printf("%v %T -> %s\n", s, s, s.Speak())

    s = Robot{ID: 7}
    fmt.Printf("%v %T -> %s\n", s, s, s.Speak())
}

%T prints the concrete type hiding inside the interface. A Speaker that has never been assigned is nil and has no type — calling Speak() on it would panic.

(One in-browser quirk: the interpreter running these boxes reports the struct's shapestruct { Name string } — where a compiled Go binary prints the type's name, main.Dog. The value and the dispatch are identical; only the label differs.)

That pairing is what makes the method call work: at runtime, Go looks at the concrete type in the interface value and dispatches to its method. This is the "virtual dispatch" that embedding (module 5) deliberately doesn't do.

Keep interfaces small

The most-quoted line in Go: the bigger the interface, the weaker the abstraction. The standard library's most useful interfaces have one method:

type Stringer interface { String() string }
type error     interface { Error() string }
type Writer    interface { Write(p []byte) (n int, err error) }
type Reader    interface { Read(p []byte) (n int, err error) }

A one-method interface is trivial to satisfy, trivial to fake in a test, and composes with everything. When you find yourself writing an interface with eight methods, you're probably describing a type rather than a requirement.

Accept interfaces, return structs

The other slogan worth internalising:

package main

import "fmt"

type Notifier interface {
    Notify(msg string) string
}

type Email struct{ To string }
type SMS struct{ Number string }

func (e Email) Notify(msg string) string { return "email to " + e.To + ": " + msg }
func (s SMS) Notify(msg string) string   { return "sms to " + s.Number + ": " + msg }

func alertAll(msg string, targets ...Notifier) {
    for _, t := range targets {
        fmt.Println(t.Notify(msg))
    }
}

func main() {
    alertAll("deploy finished",
        Email{To: "ops@example.com"},
        SMS{Number: "+15550100"},
    )
}

alertAll takes an interface, so callers can pass anything that notifies — including a fake one in a test. Constructors, on the other hand, return concrete types (*Email), so callers keep access to everything the type offers and can decide for themselves what interface it fits.

Take a parameter as an interface only when you'd genuinely accept more than one implementation. Interfaces "just in case" are a common way to make Go code harder to read for no benefit.

Your turn

Define an Animal interface with a Sound() string method, implement it for Cow and Duck, and loop over a slice of Animal printing each sound:

moo
quack
package main

import "fmt"

// define the Animal interface, Cow and Duck

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

import "fmt"

type Animal interface {
    Sound() string
}

type Cow struct{}
type Duck struct{}

func (c Cow) Sound() string  { return "moo" }
func (d Duck) Sound() string { return "quack" }

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

Next: the rules of satisfaction — including the pointer-receiver detail that trips everyone up once.