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:
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:
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:
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:
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¶
- The Go specification: Method sets ↗, Method declarations ↗, Selectors ↗, Type assertions ↗ and Type switches ↗.
- The Go FAQ, Why is my nil error value not equal to nil? ↗ — the interface mistake the compiler cannot see, on its own page.