29. Satisfying an interface: the rules

📖 Reading · 11 min
💡 Every code box below is live — edit it and hit Run.

Implicit satisfaction feels like magic until it doesn't work and you can't see why. There are exactly three rules to know, and one of them — the pointer-receiver rule — accounts for most of the confusion.

Rule 1: every method must match exactly

package main

import "fmt"

type Closer interface {
    Close() error
}

type File struct{ Name string }

func (f File) Close() error {
    fmt.Println("closing", f.Name)
    return nil
}

func main() {
    var c Closer = File{Name: "data.txt"}
    fmt.Println(c.Close())
}

Name, parameters and results must all line up. A method Close() with no result does not satisfy Close() error — Go doesn't coerce signatures, and the compile error tells you precisely which method is wrong or missing.

Rule 2: the compiler checks at assignment

Satisfaction is verified where you assign a concrete value to an interface, which is the moment you'd get an error. To assert it deliberately — so you find out at compile time even when nothing assigns yet — Go programmers use this line:

package main

import "fmt"

type Closer interface {
    Close() error
}

type File struct{ Name string }

func (f File) Close() error { return nil }

var _ Closer = File{}

func main() {
    fmt.Println("File satisfies Closer, checked at compile time")
}

var _ Closer = File{} declares a throwaway variable of the interface type and assigns a File to it. It costs nothing at runtime and breaks the build the moment File stops satisfying Closer. You'll see it near the top of files in real Go code — now you know what it's for.

Rule 3: pointer receivers mean pointers only

This is the one:

package main

import "fmt"

type Counter interface {
    Inc()
    Value() int
}

type Basic struct{ n int }

func (b *Basic) Inc()       { b.n++ }
func (b *Basic) Value() int { return b.n }

func main() {
    var c Counter = &Basic{}

    c.Inc()
    c.Inc()
    fmt.Println(c.Value())

    // var broken Counter = Basic{} // won't compile
}

Inc and Value have pointer receivers, so *Basic satisfies Counter and plain Basic does not. Uncomment that last line and the compiler says "Basic does not implement Counter (method Inc has pointer receiver)" — one of the most-Googled Go error messages there is.

Why the asymmetry? Go can always get a value from a pointer, but it can't always get a pointer from a value — a temporary like Basic{} has no address to take. Rather than sometimes working, it never does.

The practical consequence: if your methods have pointer receivers, put pointers in your interface slices.

package main

import "fmt"

type Counter interface {
    Inc()
    Value() int
}

type Basic struct {
    Name string
    n    int
}

func (b *Basic) Inc()       { b.n++ }
func (b *Basic) Value() int { return b.n }

func main() {
    counters := []Counter{
        &Basic{Name: "a"},
        &Basic{Name: "b"},
    }

    counters[0].Inc()
    counters[0].Inc()
    counters[1].Inc()

    for _, c := range counters {
        fmt.Println(c.Value())
    }
}

[]Counter{&Basic{...}} — the & is doing real work. This is the practical reason for module 5's consistency rule: pick pointer receivers for a type and stick to them, and this question only comes up once.

Value receivers work either way

The reverse direction is fine. A type with value receivers satisfies the interface as both a value and a pointer:

package main

import "fmt"

type Greeter interface {
    Greet() string
}

type Person struct{ Name string }

func (p Person) Greet() string { return "hi, " + p.Name }

func main() {
    var a Greeter = Person{Name: "Ada"}
    var b Greeter = &Person{Name: "Grace"}

    fmt.Println(a.Greet())
    fmt.Println(b.Greet())
}

Go dereferences the pointer for you. So value receivers are the more permissive choice — which is another reason small, immutable types use them.

An interface hides everything else

Once a value is inside an interface, only the interface's methods are reachable:

package main

import "fmt"

type Namer interface {
    Name() string
}

type Server struct {
    host string
    Port int
}

func (s Server) Name() string { return s.host }
func (s Server) Restart()     { fmt.Println("restarting", s.host) }

func main() {
    var n Namer = Server{host: "api", Port: 8080}

    fmt.Println(n.Name())
    // n.Restart()  // won't compile: Namer has no Restart
    // fmt.Println(n.Port) // won't compile: Namer has no fields
}

The Server is still in there, whole and unmodified — but the static type is Namer, so that's all the compiler will let you touch. Getting back to the concrete type is a type assertion, which is the next lesson.

Interfaces are types too

Since an interface is a type, it can appear anywhere a type can: as a struct field, a map value, a function result, a channel element.

package main

import "fmt"

type Validator interface {
    Validate(string) error
}

type NotEmpty struct{}
type MaxLen struct{ N int }

func (NotEmpty) Validate(s string) error {
    if s == "" {
        return fmt.Errorf("value is empty")
    }
    return nil
}

func (m MaxLen) Validate(s string) error {
    if len(s) > m.N {
        return fmt.Errorf("value is longer than %d characters", m.N)
    }
    return nil
}

type Field struct {
    Name  string
    Rules []Validator
}

func (f Field) Check(value string) {
    for _, r := range f.Rules {
        if err := r.Validate(value); err != nil {
            fmt.Printf("%s: %v\n", f.Name, err)
            return
        }
    }
    fmt.Printf("%s: ok\n", f.Name)
}

func main() {
    username := Field{Name: "username", Rules: []Validator{NotEmpty{}, MaxLen{N: 8}}}

    username.Check("ada")
    username.Check("")
    username.Check("verylongusername")
}

Note func (NotEmpty) Validate(...) — a receiver you never use can be written without a name. And Rules []Validator is a struct field holding interfaces, so the rules are configurable at runtime and trivially extensible: a new rule type needs no change to Field.

Your turn

Make *Stack satisfy this Store interface — Put appends, Size reports the count. The program should print:

2
package main

import "fmt"

type Store interface {
    Put(item string)
    Size() int
}

type Stack struct {
    items []string
}

// add Put and Size with pointer receivers

func main() {
    var s Store = &Stack{}
    s.Put("a")
    s.Put("b")
    fmt.Println(s.Size())
}
package main

import "fmt"

type Store interface {
    Put(item string)
    Size() int
}

type Stack struct {
    items []string
}

func (s *Stack) Put(item string) {
    s.items = append(s.items, item)
}

func (s *Stack) Size() int {
    return len(s.items)
}

func main() {
    var s Store = &Stack{}
    s.Put("a")
    s.Put("b")
    fmt.Println(s.Size())
}

Next: getting the concrete type back out of an interface.