Skip to content

Interfaces: the compiler errors

Level: 201 · anyone reading a refusal about a method set, a selector or an assertion

One line: Eight things the Go 1.25 compiler refuses around interfaces and methods, each with its exact wording recorded by a driver, the mistake behind it, and the fix — most of them are the method-set rule of A pointer receiver changes the method set meeting a value that was handed over in the wrong shape.

The wording is the compiler's at go1.25.5 and go1.25.14; a later release may reword a message, which is why each is recorded rather than remembered.

method Deposit has pointer receiver

A Wallet value is passed where a Depositor is wanted, and Deposit is declared on *Wallet.

Verified output of interfaces_err_pointer_receiver_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build wallet.go
# command-line-arguments
./wallet.go:15:9: cannot use w (variable of struct type Wallet) as Depositor value in argument to payday: Wallet does not implement Depositor (method Deposit has pointer receiver)
exit status 1

The mistake — Wallet's method set has no Deposit; only *Wallet's does. The message says so in its parenthesis, and names the method.

The fix — pass the address, or make the variable a pointer to begin with:

    var w Wallet
    payday(&w)

missing method, and wrong type for method

A []Shape literal holds a Square whose Area returns int, and a Circle with no Area.

Verified output of interfaces_err_missing_method_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build shapes.go
# command-line-arguments
./shapes.go:14:20: cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (wrong type for method Area)
        have Area() int
        want Area() float64
./shapes.go:14:31: cannot use Circle{…} (value of struct type Circle) as Shape value in array or slice literal: Circle does not implement Shape (missing method Area)
exit status 1

The mistake — an interface is satisfied by name and signature. Square has an Area, but not Area() float64, and the have/want lines show the two. Circle has none at all.

The fix — match the signature exactly, and give Circle the method:

func (s Square) Area() float64 { return s.Side * s.Side }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }

type Logger has no field or method Flush

l is declared as a Logger, the value inside is a *Console that can Flush, and the program calls l.Flush().

Verified output of interfaces_err_undefined_method_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build logger.go
# command-line-arguments
./logger.go:15:4: l.Flush undefined (type Logger has no field or method Flush)
exit status 1
$ go run logger_asserted.go
order 7 shipped
flushed 1 lines
exit status 0

The mistake — the static type of l is the interface, and only the interface's methods exist on it. What the dynamic value can also do is invisible until asked for.

The fix — ask at run time with a type assertion to the behaviour, as the second program does, and the Flush happens only when the value has one:

    if f, ok := l.(interface{ Flush() }); ok {
        f.Flush()
    }

Or add Flush to Logger, if every logger must have one. A type assertion asks the dynamic type is the lesson.

ambiguous selector

Contact embeds Person and Company, and both have a Name field and a Describe method.

Verified output of interfaces_err_ambiguous_selector_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build contact.go
# command-line-arguments
./contact.go:20:16: ambiguous selector c.Name
./contact.go:20:24: ambiguous selector c.Describe
exit status 1

The mistake — promotion needs exactly one candidate at the shallowest depth. With two at depth 1 and nothing at depth 0, c.Name names neither. The struct declaration itself is legal; the error appears at the use.

The fix — name the path, or declare the member on Contact so that depth 0 wins:

    fmt.Println(c.Person.Name, c.Company.Name)

func (c Contact) Describe() string { return c.Person.Describe() + " at " + c.Company.Name }

Embedding is not inheritance shows the depth rule at work.

impossible type assertion

A fmt.Stringer is asserted to Order, which has no String method; then an int variable is asserted at all.

Verified output of interfaces_err_impossible_assertion_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build assertions.go
# command-line-arguments
./assertions.go:9:14: impossible type assertion: s.(Order)
    Order does not implement fmt.Stringer (missing method String)
./assertions.go:13:14: invalid operation: quantity (variable of type int) is not an interface
exit status 1

The mistake — s.(Order) could never succeed, because an Order can never be inside a fmt.Stringer: the compiler checks the method set at compile time and refuses, in the same words the runtime would use for a missing method. quantity.(int) is a different mistake: quantity is an int, not an interface, and there is no dynamic type to ask about.

The fix — give Order the method, or assert to a type that has it; and drop the assertion on the plain int:

func (o Order) String() string { return fmt.Sprintf("order %d", o.ID) }

    n := quantity // an int is already an int

The related vet finding, an assertion between two interfaces that no type could satisfy, is ifaceassert — that one compiles.

duplicate case in type switch

Three mistakes in one file: a case type the switched value can never hold, the same type in two cases, and a bound variable no clause uses.

Verified output of interfaces_err_type_switch_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build classify.go
# command-line-arguments
./classify.go:9:7: impossible type switch case: Order
    s (variable of interface type fmt.Stringer) cannot have dynamic type Order (missing method String)
./classify.go:21:7: duplicate case int in type switch
    ./classify.go:17:7: previous case
./classify.go:29:9: x declared and not used
exit status 1

The mistake — case Order: on a fmt.Stringer is the impossible assertion above in switch form. case int: twice would leave the second unreachable, and the compiler points at the previous case. switch x := v.(type) declares x in every clause, and a variable declared and not used is an error in Go, even here.

The fix — delete the impossible and duplicate cases, and either use x in at least one clause or switch without binding:

    switch v.(type) { // no variable when no clause needs the value
    case int:
        fmt.Println("an int")
    }

invalid receiver type

Methods declared on a named pointer type, on the built-in int, and on an interface type.

Verified output of interfaces_err_receiver_type_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build receivers.go
# command-line-arguments
./receivers.go:9:9: invalid receiver type StackRef (pointer or interface type)
./receivers.go:11:9: cannot define new methods on non-local type int
./receivers.go:15:9: invalid receiver type Logger (pointer or interface type)
exit status 1

The mistake — the spec ↗ allows a receiver of type T or *T where T is a defined type in this package that is neither a pointer nor an interface. StackRef is defined as a pointer, int is not this package's, and Logger is an interface: three different refusals, and the second one is the one people meet when they want a method on int or string.

The fix — a defined type of your own, with the pointer written at the method:

type Cents int

func (c Cents) Dollars() float64 { return float64(c) / 100 }

func (s *Stack) Len() int { return len(*s) } // *Stack, not a named pointer type

need type assertion

A value read from a map[string]any is assigned to an int variable.

Verified output of interfaces_err_need_type_assertion_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build unbox.go
# command-line-arguments
./unbox.go:7:20: cannot use settings["retries"] (map index expression of interface type any) as int value in variable declaration: need type assertion
exit status 1

The mistake — an any does not turn back into an int on assignment; the dynamic type is only known at run time, and the compiler says what it needs.

The fix — assert, with the two-value form when the map might hold something else:

    retries, ok := settings["retries"].(int)
    if !ok {
        retries = 3
    }
The eight drivers

interfaces_err_pointer_receiver_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Wallet has Deposit on a pointer receiver, so only *Wallet satisfies
# Depositor. Passing a Wallet value where a Depositor is wanted is refused,
# and the message names the method and the receiver kind.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/wallet.go" <<'GO'
package main

import "fmt"

type Wallet struct{ cents int }

func (w *Wallet) Deposit(cents int) { w.cents += cents }

type Depositor interface{ Deposit(cents int) }

func payday(d Depositor) { d.Deposit(5000) }

func main() {
    var w Wallet
    payday(w)
    fmt.Println(w.cents)
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build wallet.go'
(cd "$dir" && go build wallet.go) 2>&1
echo "exit status $?"

interfaces_err_missing_method_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Two ways to fall short of an interface: Square has Area with the wrong
# result type, Circle has no Area at all. The compiler says which, and for
# the wrong signature prints what it has and what it wants.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/shapes.go" <<'GO'
package main

import "fmt"

type Shape interface{ Area() float64 }

type Square struct{ Side float64 }

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

type Circle struct{ Radius float64 }

func main() {
    shapes := []Shape{Square{2}, Circle{1}}
    fmt.Println(len(shapes))
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build shapes.go'
(cd "$dir" && go build shapes.go) 2>&1
echo "exit status $?"

interfaces_err_undefined_method_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# The static type of a variable of interface type is the interface, and only
# its methods can be called -- whatever the dynamic value can also do.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/logger.go" <<'GO'
package main

import "fmt"

type Logger interface{ Log(line string) }

type Console struct{ lines int }

func (c *Console) Log(line string) { c.lines++; fmt.Println(line) }
func (c *Console) Flush()          { fmt.Println("flushed", c.lines, "lines") }

func main() {
    var l Logger = &Console{}
    l.Log("order 7 shipped")
    l.Flush()
}
GO

cat >"$dir/logger_asserted.go" <<'GO'
package main

import "fmt"

type Logger interface{ Log(line string) }

type Console struct{ lines int }

func (c *Console) Log(line string) { c.lines++; fmt.Println(line) }
func (c *Console) Flush()          { fmt.Println("flushed", c.lines, "lines") }

func main() {
    var l Logger = &Console{}
    l.Log("order 7 shipped")
    if f, ok := l.(interface{ Flush() }); ok { // ask the dynamic value, at run time
        f.Flush()
    }
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build logger.go'
(cd "$dir" && go build logger.go) 2>&1
echo "exit status $?"

say 'go run logger_asserted.go'
(cd "$dir" && go run logger_asserted.go) 2>&1
echo "exit status $?"

interfaces_err_ambiguous_selector_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Two embedded types at the same depth both promote Name and Describe; the
# outer type has no own Name or Describe to win, so either selector is
# ambiguous. The error appears only where the selector is used.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/contact.go" <<'GO'
package main

import "fmt"

type Person struct{ Name string }

func (p Person) Describe() string { return "person " + p.Name }

type Company struct{ Name string }

func (c Company) Describe() string { return "company " + c.Name }

type Contact struct {
    Person
    Company
}

func main() {
    c := Contact{Person{"Ann Lee"}, Company{"Northwind"}}
    fmt.Println(c.Name, c.Describe())
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build contact.go'
(cd "$dir" && go build contact.go) 2>&1
echo "exit status $?"

interfaces_err_impossible_assertion_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Two assertions the compiler can already rule out: asserting a fmt.Stringer
# to a type that has no String method, and asserting on a value that is not
# an interface at all. Neither reaches the run-time check.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/assertions.go" <<'GO'
package main

import "fmt"

type Order struct{ ID int }

func main() {
    var s fmt.Stringer
    if o, ok := s.(Order); ok {
        fmt.Println(o.ID)
    }
    quantity := 3
    if n, ok := quantity.(int); ok {
        fmt.Println(n)
    }
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build assertions.go'
(cd "$dir" && go build assertions.go) 2>&1
echo "exit status $?"

interfaces_err_type_switch_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Three type-switch mistakes in one file: a case type the switched value can
# never have, the same type in two cases, and a bound variable that no case
# uses.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/classify.go" <<'GO'
package main

import "fmt"

type Order struct{ ID int }

func kind(s fmt.Stringer) string {
    switch s.(type) {
    case Order:
        return "order"
    }
    return "other"
}

func size(v any) string {
    switch v.(type) {
    case int:
        return "int"
    case string:
        return "string"
    case int:
        return "int again"
    }
    return "other"
}

func main() {
    var v any = 3
    switch x := v.(type) {
    case int:
        fmt.Println("an int")
    case string:
        fmt.Println("a string")
    }
    fmt.Println(kind(nil), size(v))
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build classify.go'
(cd "$dir" && go build classify.go) 2>&1
echo "exit status $?"

interfaces_err_receiver_type_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# What can carry a method: a named type declared in this package that is
# not itself a pointer or an interface. A named pointer type, a built-in
# type and an interface type are refused, each with its own message.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/receivers.go" <<'GO'
package main

type Stack []int

func (s *Stack) Push(v int) { *s = append(*s, v) }

type StackRef *Stack

func (s StackRef) Len() int { return len(*s) }

func (n int) Cents() int { return n * 100 }

type Logger interface{ Log(line string) }

func (l Logger) Flush() {}

func main() {}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build receivers.go'
(cd "$dir" && go build receivers.go) 2>&1
echo "exit status $?"

interfaces_err_need_type_assertion_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# A value of interface type does not turn back into its dynamic type on its
# own: assigning an any to an int needs a type assertion.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/unbox.go" <<'GO'
package main

import "fmt"

func main() {
    settings := map[string]any{"retries": 3}
    var retries int = settings["retries"]
    fmt.Println(retries)
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build unbox.go'
(cd "$dir" && go build unbox.go) 2>&1
echo "exit status $?"

Sources