Interfaces

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27. An interface is a set of method signatures, and a type satisfies it just by having those methods: there is no implements keyword, no declaration on the implementing side, and the implementing type does not even need to know the interface exists. Interfaces are Go’s only mechanism for run-time polymorphism and, together with structs and embedding, they replace the class hierarchies of other languages. The standard library is built on a handful of tiny interfaces (io.Reader, error, fmt.Stringer, http.Handler) that make unrelated packages fit together.

The page goes from the rules to the representation to the idioms: what an interface type is and how it is satisfied, how an interface value is stored (and why a nil pointer inside an interface is not a nil interface), any, type assertions and type switches, comparing interfaces, the standard interfaces every Go programmer meets, the "accept interfaces, return structs" guideline, compile-time assertions, optional interfaces, and the link with generic constraints. The previous page, Methods and Receivers, explains method sets and receivers; the next ones are Composition and Object-Oriented Design, Generics and Error Handling. For other languages' versions of the same idea see the Rust traits and trait objects pages (which share this page’s shelf domain), Java interfaces, C# interfaces, Swift protocols and TypeScript interfaces.

Every example was compiled, vetted with go vet and run with Go 1.27.2; the output shown is the real output (the compiler and vet messages are likewise copied from real runs). The go.mod of the example module is:

module example.com/shelf

go 1.27

The examples use the shelf library catalogue: the catalog package declares Book, ISBN (a defined string type with a String method), Loan, LoanState (an iota enumeration with a String method), the Store interface and the in-memory *MemStore. Store is:

// Store persists books.
type Store interface {
	Get(isbn ISBN) (Book, bool)
	Put(b Book)
	All() []Book
}

What an interface is

The specification (Interface types) says that an interface type defines a type set: the set of all types that implement the interface. A basic interface lists methods (and may embed other basic interfaces); the method set of the interface is exactly the list of its methods. A variable of interface type can hold any value whose type’s method set contains all of them, and calling a method through the variable calls the method of the value’s dynamic type.

classDiagram direction LR class Store { <<interface>> +Get(isbn) (Book, bool) +Put(b) +All() []Book } class MemStore { -mu RWMutex -books map +Get(isbn) (Book, bool) +Put(b) +All() []Book } class cached { +Store Store +hits int +Get(isbn) (Book, bool) } class fakeStore { +Get(isbn) (Book, bool) +Put(b) +All() []Book } Store <|.. MemStore : satisfies implicitly Store <|.. cached : satisfies implicitly Store <|.. fakeStore : satisfies implicitly cached o-- Store : wraps

The arrows in the diagram do not exist in the Go source: nothing in MemStore or fakeStore names Store. Satisfaction is structural, decided by the compiler from the method sets. That has three consequences that shape Go programs.

  • The interface can be declared after the type, and by someone else. A package that needs "something with a Get method" declares that interface itself; the types it will later receive, written years earlier, already satisfy it. This is why the Go guideline is to define interfaces where they are used (see below), not where the implementation lives.

  • There is no list of implementers to maintain, and no cyclic dependency between the package of the interface and the package of the type.

  • Satisfaction can happen by accident. A method called Close() error on an unrelated type satisfies io.Closer. That is deliberate (the contract is the signature), but the semantics are not checked; document them in the interface’s comment. The FAQ entry How can I guarantee my type satisfies an interface? and the one on why type T does not satisfy an Equal(T) interface state the rules from the user’s point of view.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

// Describer is satisfied by any type with a Describe method.
type Describer interface {
	Describe() string
}

// Book and Loan never mention Describer: they simply have the method.
type Book struct{ Title string }

func (b Book) Describe() string { return "book " + b.Title }

type Loan struct {
	Member string
	State  catalog.LoanState
}

func (l Loan) Describe() string { return fmt.Sprintf("loan to %s (%v)", l.Member, l.State) }

func main() {
	items := []Describer{
		Book{Title: "Dune"},
		Loan{Member: "ada", State: catalog.OnLoan},
	}
	for _, it := range items {
		fmt.Println(it.Describe())
	}
}
book Dune
loan to ada (on loan)

The precise rule: implementing an interface

A type T implements an interface I if T is not an interface and is an element of the type set of I, or if T is itself an interface and its type set is a subset of the type set of I. A value of type T implements I when T does (spec: Implementing an interface). In practice this means:

  • every method of I must exist in the method set of T with an identical signature: same name, same parameter types, same result types, and the same variadic-ness. Parameter and result names do not matter, and neither does the receiver’s name;

  • for an exported method name the package does not matter, but an unexported method name belongs to its package, so an interface with an unexported method can only be satisfied by types from the same package (this is how a package "seals" an interface);

  • an interface satisfies another interface when its method set is a superset, so a ReadWriteCloser can be assigned to a Reader variable with no conversion.

When a type does not satisfy an interface the compiler says exactly why: a missing method, a wrong signature or a pointer receiver. The three messages are worth knowing by heart.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

type readOnly struct{}

func (readOnly) Get(isbn catalog.ISBN) (catalog.Book, bool) { return catalog.Book{}, false }
func (readOnly) All() []catalog.Book                        { return nil }

func main() {
	var s catalog.Store = readOnly{} // Put is missing
	fmt.Println(s.All())
}
./main.go:15:24: cannot use readOnly{} (value of struct type readOnly) as catalog.Store value in variable declaration: readOnly does not implement catalog.Store (missing method Put)
package main

import (
	"fmt"
	"io"
)

type shouty struct{}

func (shouty) Read(p []byte) int { return 0 } // wrong signature: no error result

func main() {
	var r io.Reader = shouty{}
	fmt.Println(r)
}
./main.go:13:20: cannot use shouty{} (value of struct type shouty) as io.Reader value in variable declaration: shouty does not implement io.Reader (wrong type for method Read)
		have Read([]byte) int
		want Read([]byte) (int, error)

The have/want lines in the second message are the quickest way to spot the usual mistake: a method with the right name but a different signature (here the missing error result of Read).

Method sets decide who satisfies what

Satisfaction is decided by method sets (spec: Method sets):

Type of the value Method set contains Satisfies an interface that needs

T

methods declared with a value receiver (t T)

only value-receiver methods

*T

methods with a value receiver and with a pointer receiver

any method of T

T stored in an interface

the same as T

pointer-receiver methods are not available

an embedded field S in struct{ S }

promoted methods of S

value methods of S (and of *S when the struct is addressable)

The rule is deliberate: a pointer-receiver method needs an address to modify, and the copy inside an interface value has none that the caller could observe. The compiler therefore refuses the assignment, and the fix is to store a pointer.

package main

import "fmt"

type counter struct{ n int }

func (c *counter) Incr() { c.n++ }

type incrementer interface{ Incr() }

func main() {
	var i incrementer = counter{} // counter has no Incr: only *counter does
	i.Incr()
	fmt.Println(i)
}
./main.go:12:22: cannot use counter{} (value of struct type counter) as incrementer value in variable declaration: counter does not implement incrementer (method Incr has pointer receiver)

The *MemStore in the shelf catalogue is the usual case: its methods lock a mutex, so they have pointer receivers, and only *MemStore (never MemStore) satisfies Store. The full table of method sets, the automatic & and * on direct calls and the reasoning for choosing a receiver are in Methods and Receivers; the way embedding changes the method set of the outer type is in Structs and Embedding.

Methods are not restricted to structs: any defined type declared in the same package can have them, so slices, maps and function types satisfy interfaces too.

package main

import (
	"fmt"
	"slices"
	"sort"

	"example.com/shelf/catalog"
)

// LoanStates is a slice type with methods: any named type may implement interfaces.
type LoanStates []catalog.LoanState

func (l LoanStates) String() string { return fmt.Sprintf("%d states", len(l)) }

// Counter is a map type with a method.
type Counter map[string]int

func (c Counter) Add(k string) { c[k]++ }

type adder interface{ Add(string) }

func main() {
	var s fmt.Stringer = LoanStates{catalog.OnLoan}
	fmt.Println(s)

	c := Counter{}
	var a adder = c // map is a reference type: value receiver can mutate
	a.Add("x")
	a.Add("x")
	fmt.Println(c)

	fmt.Println(sort.IsSorted(sort.IntSlice([]int{1, 2, 3})), slices.IsSorted([]int{3, 1}))
}
1 states
map[x:2]
true false

Small interfaces and interface embedding

Go interfaces are small on purpose. The proverb "the bigger the interface, the weaker the abstraction" comes from Rob Pike’s Go Proverbs, and the standard library follows it: io.Reader, io.Writer, io.Closer, fmt.Stringer, error and sort.Interface have one to three methods each. A small interface is satisfied by many types, easy to fake in a test and cheap to document. Effective Go (Interfaces and other types) describes interfaces with one or two methods as the common case.

Larger interfaces are composed by embedding smaller ones (spec: Embedded interfaces). The method set of the new interface is the union of the embedded method sets plus any methods declared next to them.

classDiagram direction BT class Reader { <<interface>> +Read(p) (n, err) } class Writer { <<interface>> +Write(p) (n, err) } class Closer { <<interface>> +Close() error } class ReadWriter { <<interface>> } class ReadWriteCloser { <<interface>> } Reader <|-- ReadWriter Writer <|-- ReadWriter ReadWriter <|-- ReadWriteCloser Closer <|-- ReadWriteCloser
package main

import (
	"fmt"
	"strings"
)

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

// ReadWriter embeds two interfaces: its method set is the union.
type ReadWriter interface {
	Reader
	Writer
}

// ReadWriteCloser embeds ReadWriter and Closer.
type ReadWriteCloser interface {
	ReadWriter
	Closer
}

// Since Go 1.14 embedded interfaces may overlap on identical methods.
type Named interface{ Name() string }
type Titled interface{ Name() string }
type Both interface {
	Named
	Titled
}

type book struct{ title string }

func (b book) Name() string { return strings.ToUpper(b.title) }

type buf struct{ data []byte }

func (b *buf) Read(p []byte) (int, error)  { return copy(p, b.data), nil }
func (b *buf) Write(p []byte) (int, error) { b.data = append(b.data, p...); return len(p), nil }
func (b *buf) Close() error                { return nil }

func main() {
	var rwc ReadWriteCloser = &buf{}
	var rw ReadWriter = rwc // a ReadWriteCloser is assignable to its sub-interface
	fmt.Fprintf(rw, "hello")
	fmt.Println(string(rwc.(*buf).data))

	var both Both = book{"dune"}
	fmt.Println(both.Name())
}
hello
DUNE
  • The union of method sets is a union by name: two embedded interfaces may declare the same method as long as the signatures are identical. Before Go 1.14 that was a compile error (duplicate method), which is why older code and older books (Vivien, chapter 8) avoid it. Since Go 1.14 overlapping embedded interfaces are allowed, as Both shows (Go 1.14 release notes).

  • An interface can embed any interface type, including ones from other packages (io.ReadWriter embeds io.Reader and io.Writer), and since Go 1.18 can also embed type-set elements, which turns it into a constraint (see Interfaces as type sets).

  • Embedding an interface in a struct is a different feature: the struct gets the interface’s methods as promoted methods, forwarded to whatever value the field holds. That is how decorators are written (below).

The standard library composes io this way: io.ReadWriter, io.ReadCloser, io.WriteCloser, io.ReadWriteCloser, io.ReadSeeker, io.ReadWriteSeeker, io.ReadSeekCloser and io.WriteSeeker are all built from Reader, Writer, Closer and Seeker, and io.ByteReader, io.RuneReader, io.WriterTo, io.ReaderFrom, io.ReaderAt and io.WriterAt are further one- or two-method interfaces (io).

Interface values: a type and a value

A variable of interface type is not a reference to the object; it is a small value of its own that holds two things: the dynamic type of what was stored and the dynamic value. The spec describes them as the "dynamic type" and "dynamic value" of the interface value, and the Go blog’s The Laws of Reflection explains the pair; the Tour shows it with %v and %T (Interface values).

The implementation, which the spec does not require but every current gc toolchain follows, is a pair of machine words:

  • a non-empty interface such as Store or error stores a pointer to an itab (interface table): a runtime structure, built once per (interface type, concrete type) pair and cached, which holds the two types and an array of function pointers, one for each method of the interface, in alphabetical order;

  • the empty interface any has no methods to look up, so it stores the pointer to the concrete type’s descriptor directly;

  • the second word points at the stored value: a pointer type is stored directly in the word, other values are copied to an allocation (or to read-only data for constants), which is why an interface holds a copy of a non-pointer value.

Four panels. A nil interface has a nil type word and a nil data word. An interface holding a pointer has an itab word pointing at a cached table with the interface type, the concrete type and the method pointers, and a data word pointing at the pointee. An interface holding a typed nil pointer has a non-nil itab word and a nil data word, so it compares unequal to nil. An empty interface holding a Book has a type word pointing at the type descriptor and a data word pointing at a copy of the Book.

A method call through the interface (s.Get(i)) loads the function pointer from the itab and calls it with the data word as the receiver: one indirect call, which is why the compiler cannot inline it unless it can prove the dynamic type (devirtualization, which profile-guided optimization extends; see Performance and PGO).

package main

import (
	"fmt"
	"unsafe"
)

type shape interface{ Area() float64 }

type sq struct{ s float64 }

func (q sq) Area() float64 { return q.s * q.s }

func main() {
	var e any
	var s shape
	fmt.Println("sizeof any:", unsafe.Sizeof(e), "sizeof shape:", unsafe.Sizeof(s))
	fmt.Println("sizeof func value:", unsafe.Sizeof(func() {}))

	s = sq{3}
	fmt.Println(s.Area())
}
sizeof any: 16 sizeof shape: 16
sizeof func value: 8
9

Source: unsafe: Sizeof

Both any and shape are 16 bytes on a 64-bit platform, whatever they hold; a plain function value is a single word. unsafe.Sizeof reports 8 for the function here because a func value is a pointer to a closure object. These sizes are implementation facts, not guarantees.

What assigning to an interface costs

Storing a value in an interface may allocate, because the data word must point at the value. A pointer needs nothing, a small integer value and a constant often reuse static data, and a general struct is copied into a new allocation if the interface escapes (is stored somewhere that outlives the call, such as a package variable). testing.AllocsPerRun counts the allocations:

package main

import (
	"fmt"
	"testing"
)

type shape interface{ Area() float64 }

type rect struct{ w, h float64 }

func (r rect) Area() float64 { return r.w * r.h }

type big struct{ a, b, c, d int }

func (b big) Area() float64 { return float64(b.a) }

var sink shape

func main() {
	r := rect{2, 3}
	n := 1000 // not a constant, so the compiler cannot place the value in read-only data
	fmt.Println("direct call allocs:", testing.AllocsPerRun(100, func() { _ = r.Area() }))
	fmt.Println("boxing a struct allocs:", testing.AllocsPerRun(100, func() { n++; sink = big{n, 2, 3, 4} }))
	fmt.Println("boxing a pointer allocs:", testing.AllocsPerRun(100, func() { sink = &r }))
	fmt.Println("boxing a rect that escapes allocs:", testing.AllocsPerRun(100, func() { n++; sink = rect{float64(n), 1} }))
}
direct call allocs: 0
boxing a struct allocs: 1
boxing a pointer allocs: 0
boxing a rect that escapes allocs: 1

The lesson is not "avoid interfaces": a one-allocation conversion on a path that runs a few thousand times a second is nothing. It is to know that boxing exists when a profile shows allocation in a hot loop, and that APIs taking any (fmt.Println, slog attributes, sync.Map) box their arguments. The measurement tools are in Benchmarks, Fuzzing and synctest and the escape analysis in Memory Management and Garbage Collection.

The nil interface and the nil pointer in an interface

An interface value is nil only when both words are nil: no dynamic type and no value. Assigning a nil pointer of a concrete type to an interface produces a non-nil interface (the type word is set) that holds a nil pointer. This is the most famous gotcha in the language: the FAQ gives it its own entry, Why is my nil error value not equal to nil?.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

func main() {
	var s fmt.Stringer // nil interface: no type, no value
	fmt.Printf("%T %v %t\n", s, s, s == nil)

	s = catalog.OnLoan // dynamic type catalog.LoanState, value 1
	fmt.Printf("%T %v %t\n", s, s, s == nil)

	isbn, _ := catalog.ParseISBN("978-0-306-40615-7")
	s = isbn // dynamic type catalog.ISBN, value is a string
	fmt.Printf("%T %v\n", s, s)

	var p *catalog.MemStore  // nil pointer of a concrete type
	var st catalog.Store = p // interface now holds (*catalog.MemStore, nil)
	fmt.Printf("%T %t\n", st, st == nil)
}
<nil> <nil> true
catalog.LoanState on loan false
catalog.ISBN 9780306406157
*catalog.MemStore false

The same thing happens with the most common interface in Go, error:

package main

import (
	"errors"
	"fmt"
)

type ParseError struct{ Line int }

func (e *ParseError) Error() string { return fmt.Sprintf("parse error on line %d", e.Line) }

// bad returns a typed nil pointer wrapped in a non-nil error interface.
func bad(fail bool) error {
	var perr *ParseError
	if fail {
		perr = &ParseError{Line: 3}
	}
	return perr
}

// good returns the nil interface explicitly.
func good(fail bool) error {
	if fail {
		return &ParseError{Line: 3}
	}
	return nil
}

func main() {
	err := bad(false)
	fmt.Println("bad(false) == nil:", err == nil)
	fmt.Printf("dynamic type %T, value %v\n", err, err == (*ParseError)(nil))

	err = good(false)
	fmt.Println("good(false) == nil:", err == nil)

	var pe *ParseError
	fmt.Println(errors.As(bad(true), &pe), pe.Line)
}
bad(false) == nil: false
dynamic type *main.ParseError, value true
good(false) == nil: true
true 3

bad returns an error interface holding (*ParseError)(nil); the check err != nil in the caller is true, the caller tries to handle an error that does not exist, and, if the handler calls err.Error(), the method runs with a nil receiver and may panic. The rules to stay out of trouble are short:

  • a function that returns error must return the literal nil for success, never a variable of a concrete pointer type. Declare var err error, not var err *ParseError, in functions that build an error incrementally;

  • a method can be called on a nil pointer if its body handles it (Tour, methods/12), so a typed nil in an interface is not by itself a crash; it is an interface that looks like "there is a value" while the value is empty;

  • nilness and other analyzers find some of these patterns (see vet and the compiler), but not all of them, so review function results that are declared as a concrete pointer type and returned as an interface.

Bodner (chapter 7) and Vivien (chapter 8) both explain the pair; the Bodner chapter on errors (chapter 8) repeats the caveat for custom error types. The error-specific side (wrapping, errors.Is, errors.As) is in Error Handling.

Calling a method on a nil interface

If the interface itself is nil (both words nil), calling a method has nothing to dispatch to and panics with a run-time error; there is no NullPointerException-style message naming the method.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

type speaker interface{ Speak() string }

type cat struct{ name string }

func (c *cat) Speak() string { return c.name + " says meow" }

type dog struct{ name string }

func (d dog) Speak() string { return d.name + " says woof" }

func main() {
	defer func() { fmt.Println("recovered:", recover()) }()

	// a method value binds its receiver when it is evaluated:
	// for a pointer it binds the pointer, for a value it binds a copy
	c := &cat{name: "tom"}
	d := dog{name: "rex"}
	var s1, s2 speaker = c, d
	f1, f2 := s1.Speak, s2.Speak
	c.name, d.name = "felix", "fido"
	fmt.Println(f1())
	fmt.Println(f2())

	// method expression on an interface type: the receiver becomes the first argument
	g := speaker.Speak
	fmt.Println(g(d))

	// calling a method on a nil interface panics
	var st catalog.Store
	_ = st.All()
}
felix says meow
rex says woof
fido says woof
recovered: runtime error: invalid memory address or nil pointer dereference

The program also shows the two flavours of method value: s1.Speak bound the pointer (so it saw the rename), while s2.Speak bound a copy of the dog made when the method value was evaluated (so it did not), and the method expression speaker.Speak turns the method into a function whose first parameter is the receiver. The related spec sections are Method values and Method expressions.

any and the empty interface

An interface with no methods is satisfied by every type. Since Go 1.18 it has the predeclared name any, defined as an alias for interface{} (Predeclared identifiers, Go 1.18 release notes); new code writes any, and the go fix modernizers (Go 1.26) rewrite the old spelling. The two predeclared interface types besides any are error and comparable.

A value of type any can hold anything, and it tells the compiler nothing about it, so it must be inspected at run time with a type assertion or a type switch before use. The old advice stays right: "`any` says nothing". Reach for it only where the data is dynamically typed by nature: decoded JSON of unknown shape, the arguments of fmt.Printf, the values of a heterogeneous container, a plugin boundary. Everywhere else a small interface or a type parameter documents the requirement in the signature.

A frequent beginner mistake is to think that []T converts to []any implicitly. It does not: the two slices have different element sizes and layouts, and there is no implicit conversion (FAQ, Can I convert a [T to an []interface{}?]).

package main

import "fmt"

func main() {
	titles := []string{"Dune", "Ubik"}
	fmt.Println(titles...) // []string is not []any
}
./main.go:7:14: cannot use titles (variable of type []string) as []any value in argument to fmt.Println

Source: Go FAQ: Can I convert a [T to an []interface{}?]

Convert element by element, then spread with ...:

package main

import (
	"fmt"
	"strings"
)

func main() {
	titles := []string{"Dune", "Ubik", "Solaris"}

	// []string is not []any, even though string is assignable to any:
	// the two slices have different memory layouts, so convert element by element.
	args := make([]any, len(titles))
	for i, t := range titles {
		args[i] = strings.ToUpper(t)
	}
	fmt.Println(args...)
	fmt.Println(len(args), cap(args))
}
DUNE UBIK SOLARIS
3 3

Type assertions

A type assertion x.(T) asks what is inside an interface value x (Type assertions). It has two forms.

Form When T is a concrete type When T is an interface type

v := x.(T)

asserts that the dynamic type is identical to T; panics if not

asserts that the dynamic type implements T; panics if not

v, ok := x.(T)

ok reports success; on failure v is the zero value of T and nothing panics

same, with v the zero interface value on failure

Rules to remember:

  • x must have interface type. Asserting on a non-interface value is a compile error; convert to any first if you really need to.

  • The single-value form panics with a *runtime.TypeAssertionError whose message names both types; the panic is recoverable but should be treated as a programming error.

  • The comma-ok form is the one to use whenever the type is not guaranteed.

  • If T is an interface, the assertion checks the method set at run time, which is how optional interfaces work.

  • The compiler rejects an assertion that can never succeed:

package main

import (
	"fmt"
	"io"
)

type book struct{ title string }

func main() {
	var r io.Reader
	b, ok := r.(book) // book has no Read method: this can never succeed
	fmt.Println(b, ok)

	switch r.(type) {
	case book:
	}
}
./main.go:12:11: impossible type assertion: r.(book)
	book does not implement io.Reader (missing method Read)
./main.go:16:7: impossible type switch case: book
	r (variable of interface type io.Reader) cannot have dynamic type book (missing method Read)
package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

func main() {
	var v any = catalog.OnLoan

	// comma-ok form: no panic, ok reports success
	if s, ok := v.(catalog.LoanState); ok {
		fmt.Println("LoanState", int(s), s)
	}
	n, ok := v.(int)
	fmt.Println("int?", n, ok) // zero value and false

	// assertion to an interface type checks the method set at run time
	if st, ok := v.(fmt.Stringer); ok {
		fmt.Println("Stringer:", st.String())
	}

	// single-value form panics when the assertion fails
	defer func() {
		r := recover()
		fmt.Println("recovered:", r)
		if e, ok := r.(error); ok {
			fmt.Printf("%T\n", e)
		}
	}()
	_ = v.(string)
}
LoanState 1 on loan
int? 0 false
Stringer: on loan
recovered: interface conversion: interface {} is catalog.LoanState, not string
*runtime.TypeAssertionError

The assertion v.(string) panics with interface conversion: interface {} is catalog.LoanState, not string; the message still spells the empty interface the old way. A type-set interface (see below) cannot be the target type of an assertion, only basic interfaces can.

Type switches

A type switch compares the dynamic type of an interface value with several types in one statement (spec: Type switches). It is written like a switch whose tag is x.(type), with an optional short variable declaration switch v := x.(type).

  • each case lists one or more types (or nil); the first case that matches is chosen, and there is no fall-through;

  • in a single-type case the variable v has that type; in a multi-type case, or default, it keeps the type of x;

  • a case nil matches only a nil interface (not an interface holding a nil pointer);

  • a case with an interface type matches any dynamic type that implements it, so order cases from most specific to least specific (error before fmt.Stringer if a type could implement both);

  • the compiler rejects impossible and duplicate cases, and an unused v in every clause is the same "declared and not used" error as any other variable.

package main

import (
	"fmt"
	"time"

	"example.com/shelf/catalog"
)

func describe(v any) string {
	switch x := v.(type) {
	case nil:
		return "nil interface"
	case int, int64: // several types: x keeps the interface type any
		return fmt.Sprintf("integer %v (%T)", x, x)
	case string:
		return "string of length " + fmt.Sprint(len(x))
	case catalog.ISBN: // concrete defined type
		return "isbn " + string(x)
	case error: // interface case, matched by method set
		return "error: " + x.Error()
	case fmt.Stringer:
		return "stringer: " + x.String()
	case []catalog.Book:
		return fmt.Sprintf("%d books", len(x))
	case func() time.Time:
		return "a clock function"
	default:
		return fmt.Sprintf("something else: %T", x)
	}
}

func main() {
	var e error
	for _, v := range []any{
		nil, 42, int64(7), "dune", catalog.ISBN("9780306406157"),
		fmt.Errorf("boom"), catalog.Overdue, []catalog.Book{{}, {}},
		time.Now, 3.14, e,
	} {
		fmt.Println(describe(v))
	}
}
nil interface
integer 42 (int)
integer 7 (int64)
string of length 4
isbn 9780306406157
error: boom
stringer: overdue
2 books
a clock function
something else: float64
nil interface

The last line is the nil interface e; the variable is a nil error, so case nil catches it. Duplicate and impossible cases are compile errors:

package main

import "fmt"

func kind(v any) string {
	switch v.(type) {
	case int:
		return "int"
	case string, int:
		return "dup"
	}
	return ""
}

func main() { fmt.Println(kind(1)) }
./main.go:9:15: duplicate case int in type switch
	./main.go:7:7: previous case

When to use assertions and switches, and when not to

Type switches are idiomatic at the edges of a program, where data genuinely arrives as any: decoding (encoding/json into any yields map[string]any, []any, float64, string, bool or nil), formatting, parsing syntax trees, handling the several error types a call may return. In the middle of a program they are usually a sign that an interface method is missing: a switch on the type of an argument is "polymorphism by hand", and it must be updated every time a type is added, while an interface method is found by the compiler. The guideline is to prefer a method, then a small closed set of types in a switch, and a type parameter when the operation is the same for every type.

The standard interfaces

A Go programmer learns a few dozen standard interfaces early, because they are the "plugs" of the ecosystem: a type that implements io.Reader can be given to any function that reads, and so on.

Interface Method(s) Typical implementers Consumed by

fmt.Stringer

String() string

catalog.ISBN, catalog.LoanState, time.Time, net/url.URL

fmt verbs %v and %s, slog, log

error

Error() string

*os.PathError, *json.SyntaxError, your own error types

every function returning a failure; errors.Is/errors.As

io.Reader

Read(p []byte) (n int, err error)

*os.File, *strings.Reader, *bytes.Buffer, net.Conn, http.Request.Body

io.Copy, io.ReadAll, bufio, json.NewDecoder

io.Writer

Write(p []byte) (n int, err error)

*os.File, *bytes.Buffer, http.ResponseWriter, *strings.Builder

fmt.Fprintf, io.Copy, json.NewEncoder, log.New

io.Closer

Close() error

files, connections, bodies

defer cleanup; io.ReadCloser and relatives

sort.Interface

Len, Less, Swap

your sortable slice types

sort.Sort, sort.Stable, container/heap (with Push/Pop)

http.Handler

ServeHTTP(ResponseWriter, *Request)

your handlers, http.HandlerFunc, *http.ServeMux

http.Server, middleware chains

encoding.TextMarshaler, TextUnmarshaler

MarshalText() ([]byte, error), UnmarshalText([]byte) error

time.Time, net/netip.Addr, your enums and IDs

encoding/json (also as map keys), flag.TextVar, encoding/xml

json.Marshaler

MarshalJSON() ([]byte, error)

types with a custom JSON shape

encoding/json

driver.Valuer, sql.Scanner

Value(), Scan(src any) error

custom column types

database/sql

context.Context

Deadline, Done, Err, Value

the contexts of the context package

every cancellable API

fmt.Formatter, fmt.GoStringer

Format(f State, verb rune), GoString() string

types with custom %v or %#v

fmt

heap.Interface, flag.Value, io/fs.FS, slog.Handler

see the packages

custom heaps, flag types, file systems, log backends

the respective packages

Notes on the table:

  • Stringer is not built in (a common misreading of older books; Vivien chapter 1): it is fmt.Stringer, and the only predeclared interface types are any, error and comparable.

  • A method named String, Error, Read, Write, ReadByte, Format, Scan, MarshalJSON and so on is a commitment: the stdmethods check of go vet reports a method with a well-known name and the wrong signature.

  • sort.Interface is the oldest of the group and the heaviest (three methods per type). Since Go 1.21 slices.Sort, slices.SortFunc and slices.SortStableFunc sort any slice with a comparison function and no boilerplate; new code uses them, and sort.Interface remains useful for data structures that are not slices, for container/heap, and for sort.Reverse. The slices and cmp packages are covered in Collections and Utility Packages.

package main

import (
	"fmt"
	"slices"
	"sort"
	"strings"

	"example.com/shelf/catalog"
)

// byTitle implements sort.Interface (the classic three methods).
type byTitle []catalog.Book

func (s byTitle) Len() int           { return len(s) }
func (s byTitle) Less(i, j int) bool { return s[i].Title < s[j].Title }
func (s byTitle) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func titles(bs []catalog.Book) []string {
	var out []string
	for _, b := range bs {
		out = append(out, b.Title)
	}
	return out
}

func main() {
	books := []catalog.Book{{Title: "Neuromancer"}, {Title: "Dune"}, {Title: "Ubik"}}

	sort.Sort(byTitle(books))
	fmt.Println(titles(books))

	sort.Sort(sort.Reverse(byTitle(books)))
	fmt.Println(titles(books))

	// Since Go 1.21 a func and generics replace the interface in new code.
	slices.SortFunc(books, func(a, b catalog.Book) int {
		return strings.Compare(a.Title, b.Title)
	})
	fmt.Println(titles(books))
}
[Dune Neuromancer Ubik]
[Ubik Neuromancer Dune]
[Dune Neuromancer Ubik]

Source: sort: Interface

fmt.Stringer and error

String() and Error() are the two methods most types get. When fmt formats a value with %v or %s it asks, in order, whether the value implements fmt.Formatter, then (for %#v) fmt.GoStringer, then (for %v, %s, %q, %x, %X and others) error, then fmt.Stringer (fmt: Printing). A common bug is a String method that calls fmt.Sprintf("%v", x) on its own receiver, which recurses forever; convert to the underlying type first (string(i), int(s)). The shelf LoanState does that.

package main

import (
	"errors"
	"fmt"
	"os"

	"example.com/shelf/catalog"
)

// LoanError carries structured context; *LoanError implements error.
type LoanError struct {
	ISBN   catalog.ISBN
	Reason string
}

func (e *LoanError) Error() string { return fmt.Sprintf("loan %s: %s", e.ISBN, e.Reason) }

func lend(isbn catalog.ISBN) error {
	return &LoanError{ISBN: isbn, Reason: "already on loan"}
}

func main() {
	isbn, _ := catalog.ParseISBN("978-0-306-40615-7")

	// fmt.Stringer: Println, %v and %s call String
	fmt.Println(isbn, catalog.OnLoan)
	fmt.Printf("%v %s %d %q\n", catalog.Overdue, catalog.Overdue, catalog.Overdue, catalog.Overdue)

	// error: the one-method interface for failures
	err := lend(isbn)
	fmt.Println(err)

	var le *LoanError
	if errors.As(err, &le) {
		fmt.Println("reason:", le.Reason)
	}
	fmt.Fprintln(os.Stdout, "done")
}
9780306406157 on loan
overdue overdue 2 "overdue"
loan 9780306406157: already on loan
reason: already on loan
done

Source: fmt: Stringer

*LoanError has a pointer receiver, so lend returns &LoanError{...}; callers that want the fields use errors.As (see below and Error Handling). The stringer tool from golang.org/x/tools generates String methods for iota enumerations (Variables, Constants and iota).

io.Reader, io.Writer and io.Closer

io.Reader and io.Writer are the most reused interfaces in the standard library, because they describe a stream of bytes without saying where it comes from. Read fills the slice it is given and returns the number of bytes and an error; a reader may return n > 0 and an error (including io.EOF) in the same call, so callers handle the bytes before the error. io.Copy, io.ReadAll, io.MultiWriter, io.TeeReader, io.LimitReader, io.Pipe and bufio compose them. The File page (I/O, Files and the Filesystem) covers them in depth; here is the smallest pipeline:

package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

// upper wraps a Reader and upper-cases what it reads.
type upper struct{ r io.Reader }

func (u upper) Read(p []byte) (int, error) {
	n, err := u.r.Read(p)
	copy(p, bytes.ToUpper(p[:n]))
	return n, err
}

// countWriter counts the bytes written through it.
type countWriter struct {
	w io.Writer
	n int
}

func (c *countWriter) Write(p []byte) (int, error) {
	n, err := c.w.Write(p)
	c.n += n
	return n, err
}

func main() {
	src := strings.NewReader("the left hand of darkness\n")
	cw := &countWriter{w: os.Stdout}

	n, err := io.Copy(cw, upper{src}) // Reader -> Writer pipeline
	fmt.Println(n, err, cw.n)

	// io.MultiWriter fans out to several Writers
	var a, b bytes.Buffer
	mw := io.MultiWriter(&a, &b)
	fmt.Fprint(mw, "dune")
	fmt.Println(a.String(), b.String())

	// io.ReadAll and the interface compositions
	var rc io.ReadCloser = io.NopCloser(strings.NewReader("abc"))
	data, _ := io.ReadAll(rc)
	fmt.Println(string(data), rc.Close())
}
THE LEFT HAND OF DARKNESS
26 <nil> 26
dune dune
abc <nil>

The upper type and countWriter implement the interfaces with a few lines each, wrapping another Reader or Writer; the wrappers can be stacked (upper{bufio.NewReader(...)}) because each knows only the interface of the one inside. This "wrapper that takes and satisfies the same interface" is the Go form of the decorator pattern.

http.Handler and the function-type adapter

http.Handler has one method, so a struct with a ServeHTTP method is a handler. Writing a type just to hold one function would be noise, so net/http declares type HandlerFunc func(ResponseWriter, *Request) with a method that calls the function; converting a plain function to HandlerFunc produces a Handler. That is the adapter idiom: a function type with a method that satisfies an interface.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

// A struct can implement http.Handler...
type health struct{ version string }

func (h health) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "ok %s", h.version)
}

// ...and a function type can too: this is the adapter pattern.
// http.HandlerFunc is declared exactly like this in net/http.
type handlerFunc func(http.ResponseWriter, *http.Request)

func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w, r) }

func main() {
	mux := http.NewServeMux()
	mux.Handle("GET /health", health{version: "1.0"})
	mux.Handle("GET /hello/{name}", handlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "hello %s", r.PathValue("name"))
	}))
	mux.HandleFunc("GET /bye", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "bye")
	})

	for _, path := range []string{"/health", "/hello/ada", "/bye", "/missing"} {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
		fmt.Println(path, rec.Code, rec.Body.String())
	}
}
/health 200 ok 1.0
/hello/ada 200 hello ada
/bye 200 bye
/missing 404 404 page not found

mux.HandleFunc is the same conversion done for you. The patterns with a method and {name} wildcards, r.PathValue and the 404 message come from the Go 1.22 ServeMux; the server side is in net/http Clients and Servers. Bodner (chapter 7) and the Go blog use HandlerFunc as the standard example of "functions can have methods".

encoding.TextMarshaler

encoding.TextMarshaler and TextUnmarshaler let a type decide how it appears as text. encoding/json uses them for values and map keys that have no other JSON encoding, flag.TextVar uses them for command-line flags, and encoding/xml and database/sql have similar hooks. The compile-time assertions after the type are discussed below.

package main

import (
	"encoding"
	"encoding/json"
	"fmt"

	"example.com/shelf/catalog"
)

// Code is a two-letter language code that marshals as text.
type Code string

func (c Code) MarshalText() ([]byte, error) { return []byte("lang:" + string(c)), nil }

func (c *Code) UnmarshalText(b []byte) error {
	if len(b) < 5 || string(b[:5]) != "lang:" {
		return fmt.Errorf("bad code %q", b)
	}
	*c = Code(b[5:])
	return nil
}

var (
	_ encoding.TextMarshaler   = Code("")
	_ encoding.TextUnmarshaler = (*Code)(nil)
)

type Edition struct {
	ISBN catalog.ISBN `json:"isbn"`
	Lang Code         `json:"lang"`
}

func main() {
	b, err := json.Marshal(Edition{ISBN: "9780306406157", Lang: "en"})
	fmt.Println(string(b), err)

	// TextMarshaler also drives map keys
	m := map[Code]int{"en": 3, "es": 1}
	b, _ = json.Marshal(m)
	fmt.Println(string(b))

	var e Edition
	err = json.Unmarshal([]byte(`{"isbn":"1","lang":"lang:fr"}`), &e)
	fmt.Println(e, err)
	err = json.Unmarshal([]byte(`{"lang":"fr"}`), &e)
	fmt.Println(err)
}
{"isbn":"9780306406157","lang":"lang:en"} <nil>
{"en":3,"es":1}
{1 fr} <nil>
bad code "fr"

Marshalers use a value receiver (so both Code and *Code satisfy TextMarshaler), unmarshalers a pointer receiver (they must modify the value). That split, a value-receiver method on the write side and a pointer-receiver one on the read side, is the normal pattern; see encoding/json for the full marshaling rules.

Comparing interface values

Interface values are comparable with == and != when the dynamic types are comparable: two interface values are equal if they have identical dynamic types and equal dynamic values, or both are nil (Comparison operators). Consequently:

  • any(1) == any(int64(1)) is false: different dynamic types;

  • comparing an interface value with a non-interface value compares the dynamic type and value, provided the non-interface type implements the interface;

  • if the two dynamic types are identical and not comparable (slices, maps, functions, and structs or arrays that contain them), the comparison panics at run time. The compiler cannot catch it, because the dynamic type is unknown, and go vet does not either;

  • interface types can be map keys and used in switch cases, and the panic risk comes with them;

  • the predeclared constraint comparable is satisfied by interface types since Go 1.20 (spec: "strictly comparable" versus "comparable"), because the comparison may panic at run time. Code that wants to guarantee no run-time panic must use a concrete type argument.

package main

import (
	"fmt"
)

type point struct{ x, y int }

func main() {
	var a, b any = point{1, 2}, point{1, 2}
	fmt.Println(a == b) // same dynamic type, equal values

	var c any = 1
	var d any = int64(1)
	fmt.Println(c == d) // different dynamic types: never equal

	var e, f any = "x", "x"
	fmt.Println(e == f)

	// map keys of interface type work for comparable dynamic types
	m := map[any]string{1: "int", "1": "string", point{0, 0}: "origin"}
	fmt.Println(m[1], m["1"], m[point{0, 0}], len(m))

	// comparing non-comparable dynamic types panics at run time
	defer func() { fmt.Println("recovered:", recover()) }()
	var g, h any = []int{1}, []int{1}
	fmt.Println(g == h)
}
true
false
true
int string origin 3
recovered: runtime error: comparing uncomparable type []int

The message comparing uncomparable type []int is the run-time error to recognise. The same panic appears when an interface holding a slice is used as a map key, as the key of a switch over interface values, or inside a struct that is compared. See also Operators and Expressions for which types are comparable and Maps for the key rules.

Accept interfaces, return structs

The best-known guideline about interfaces is Bodner’s chapter 7 formulation of an older Go proverb: accept interfaces, return structs. The Go code review wiki says the same (Go Code Review Comments: Interfaces): Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values; the implementing package returns concrete types, so new methods can be added without requiring extensive refactoring.

flowchart LR subgraph consumer["Consumer package (uses the interface)"] direction TB I["type bookGetter interface (Get)"] F["func Lend(g bookGetter, ...)"] F --> I end subgraph provider["Provider package (catalog)"] S["type MemStore struct"] N["func NewMemStore() *MemStore"] N --> S end subgraph tests["Test double"] D["type fakeGetter map"] end S -. satisfies .-> I D -. satisfies .-> I

The consequences, with the shelf example below:

  • Functions take the narrowest interface that works. Lend needs only Get, so it declares bookGetter, one method, and not the whole catalog.Store. Any type with a Get method works, from the production *MemStore to a one-line test double, and the function cannot accidentally call Put.

  • Constructors return the concrete type. NewMemStore returns *MemStore; the caller may assign it to any interface that fits, and may also use the concrete type’s extra methods. A constructor that returns the interface hides the type, prevents adding methods, and forces every caller to depend on the interface the provider chose.

  • Define the interface where it is used, not next to the implementation. A package should export an interface when it consumes it itself (like io.Reader), or when many implementations are expected to live in many packages (http.Handler, sql/driver).

  • Do not define an interface before there are two implementations or a test that needs one. The Go FAQ and the review comments both warn against interfaces "for mocking" in the package that provides the implementation.

  • Exceptions exist, and they are good ones: when a function must hide a concrete type for correctness (errors.New returns error, context.WithCancel returns Context), when a constructor returns one of several implementations chosen at run time (crypto/sha256.New() hash.Hash), and when the type is deliberately closed.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

// The consumer declares only what it needs (one method), not the whole Store.
type bookGetter interface {
	Get(isbn catalog.ISBN) (catalog.Book, bool)
}

// Lend accepts an interface...
func Lend(g bookGetter, isbn catalog.ISBN, member string) (catalog.Loan, error) {
	if _, ok := g.Get(isbn); !ok {
		return catalog.Loan{}, fmt.Errorf("lend %s: %w", isbn, errNotFound)
	}
	return catalog.Loan{Book: isbn, Member: member, State: catalog.OnLoan}, nil
}

var errNotFound = fmt.Errorf("book not found")

// ...and a constructor returns the concrete struct, never the interface.
func NewStore() *catalog.MemStore { return catalog.NewMemStore() }

// A test double needs only one method.
type fakeGetter map[catalog.ISBN]catalog.Book

func (f fakeGetter) Get(i catalog.ISBN) (catalog.Book, bool) { b, ok := f[i]; return b, ok }

func main() {
	s := NewStore()
	s.Put(catalog.Book{ISBN: "9780306406157", Title: "Dune"})

	fmt.Println(Lend(s, "9780306406157", "ada"))
	fmt.Println(Lend(s, "0000000000000", "ada"))
	fmt.Println(Lend(fakeGetter{"1": {Title: "Fake"}}, "1", "bob"))
}
{9780306406157 ada on loan} <nil>
{  available} lend 0000000000000: book not found
{1 bob on loan} <nil>

The first call prints a loan; the second wraps errNotFound with %w, so the wrapping is visible to errors.Is; the third gives a fake. The zero Loan printed as { available} is the zero value (empty ISBN, empty member, Available). Bodner (chapter 7) builds the same argument with dependency injection by interface; the pattern, together with function types as interface bridges, is the subject of Composition and Object-Oriented Design, and testing uses the same idea for fakes (Testing).

Compile-time assertions

Because satisfaction is implicit, a type that intends to satisfy an interface could silently stop doing so when someone renames a method. The FAQ’s idiom, How can I guarantee my type satisfies an interface?, turns that into a compile error with a blank-identifier variable declaration:

// the build fails if *MemStore stops satisfying Store
var _ Store = (*MemStore)(nil)

(*MemStore)(nil) is a typed nil pointer, a constant expression that allocates nothing; the variable is blank, so the declaration produces no code. Use it when the relationship is part of the public contract (an exported type documented as a Store, a handler, a marshaler), and skip it inside a package where any use of the type as the interface already proves it. A failure reads exactly like the earlier errors (missing method Put, method Put has pointer receiver).

Embedding an interface in a struct

A struct can embed an interface value. All of the interface’s methods are promoted, forwarded to the wrapped value, and the struct can override just the ones it cares about: the cheapest decorator Go offers.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

type cached struct {
	catalog.Store // embedded interface: every method is promoted
	hits          int
}

// Override only Get; Put and All are forwarded to the wrapped Store.
func (c *cached) Get(i catalog.ISBN) (catalog.Book, bool) {
	c.hits++
	return c.Store.Get(i)
}

// compile-time assertions: the build fails if a method is missing or has the
// wrong signature, and nothing exists at run time.
var (
	_ catalog.Store = (*cached)(nil)
	_ catalog.Store = (*catalog.MemStore)(nil)
)

func main() {
	c := &cached{Store: catalog.NewMemStore()}
	c.Put(catalog.Book{ISBN: "1", Title: "Dune"})
	c.Get("1")
	c.Get("2")
	fmt.Println(c.hits, len(c.All()))

	// the zero value has a nil embedded interface: calling through it panics
	defer func() { fmt.Println("recovered:", recover()) }()
	var z cached
	z.Put(catalog.Book{})
}
2 1
recovered: runtime error: invalid memory address or nil pointer dereference

cached satisfies catalog.Store although it declares only Get; Put and All come from the embedded field. The last lines show the trap: the embedded interface of a zero-value cached is nil, so the promoted Put dereferences a nil interface and panics. Always initialize the field (a constructor helps) or document that the zero value is not usable. The pattern, and its limits (embedding is not inheritance: the wrapped Store never calls back into cached), are detailed in Structs and Embedding and Composition and Object-Oriented Design.

Optional interfaces

The comma-ok assertion to an interface type tests at run time whether a value has more capabilities than its static type promises. The standard library uses it for upgrades that keep the main interfaces small:

  • io.Copy prefers io.WriterTo on the source and io.ReaderFrom on the destination, falling back to a buffer loop;

  • net/http handlers check a ResponseWriter for http.Flusher, http.Hijacker and http.Pusher (new code uses http.NewResponseController, Go 1.20, which also unwraps middleware wrappers);

  • fmt checks for Formatter, Stringer, error and GoStringer;

  • errors.Is and errors.As check for Is(error) bool, As(any) bool and Unwrap() error (or Unwrap() []error);

  • database/sql checks drivers for the optional driver.*Context interfaces.

package main

import (
	"fmt"
	"io"
	"os"
	"strings"
)

// flusher is an optional interface: a Writer may or may not support it.
type flusher interface{ Flush() error }

type logWriter struct{ w io.Writer }

func (l logWriter) Write(p []byte) (int, error) { return l.w.Write(p) }
func (l logWriter) Flush() error {
	fmt.Fprintln(l.w, "[flushed]")
	return nil
}

// write uses a plain Writer but upgrades when the value supports Flush.
func write(w io.Writer, s string) {
	fmt.Fprintln(w, s)
	if f, ok := w.(flusher); ok {
		f.Flush()
	}
}

func main() {
	write(os.Stdout, "plain")
	write(logWriter{os.Stdout}, "buffered")

	// the standard library uses the same pattern: io.Copy upgrades to WriterTo
	var sb strings.Builder
	n, _ := io.Copy(&sb, strings.NewReader("copy me"))
	_, isWT := any(strings.NewReader("")).(io.WriterTo)
	fmt.Println(n, sb.String(), isWT)
}
plain
buffered
[flushed]
7 copy me true

Source: io: WriterTo

When you wrap a value (a middleware around a ResponseWriter, a Reader decorator) the wrapper hides the optional methods of the inner value unless it forwards them; that is why http.ResponseController and the Unwrap convention exist. Define an optional interface of your own when callers can function without it but a better path exists when it is present.

Interfaces as type sets

Since Go 1.18 an interface also defines a type set that can be restricted with a union of types or the approximation element ~T, and such an interface is a constraint for type parameters (spec: General interfaces). Two kinds exist:

  • a basic interface: only methods (and embedded basic interfaces). It can be used as a variable type, a field type or a constraint;

  • a general interface: contains a union, a ~T term or a non-interface type, or embeds comparable. It can only be used as a constraint.

package main

import (
	"fmt"
	"strconv"

	"example.com/shelf/catalog"
)

// Number is a general interface (type set), usable only as a constraint.
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](xs ...T) T {
	var s T
	for _, x := range xs {
		s += x
	}
	return s
}

// A basic interface used as a constraint: T must have String.
func Join[T fmt.Stringer](xs []T) string {
	out := ""
	for i, x := range xs {
		if i > 0 {
			out += ", "
		}
		out += x.String()
	}
	return out
}

type Count int

func (c Count) String() string { return "#" + strconv.Itoa(int(c)) }

func main() {
	fmt.Println(Sum(1, 2, 3), Sum(1.5, 2.5), Sum(Count(4), Count(5)))
	fmt.Println(Join([]catalog.LoanState{catalog.Available, catalog.Overdue}))
	fmt.Println(Join([]Count{1, 2}))
}
6 4 #9
available, overdue
#1, #2

Join uses the basic interface fmt.Stringer as a constraint: the type parameter must have String, and the call is a static call, not an interface dispatch. Sum uses a union with ~int, so Count (whose underlying type is int) qualifies. The error for misuse is a clear one:

package main

import "fmt"

type Number interface {
	~int | ~float64
}

func main() {
	var n Number = 3 // a type-set interface is only a constraint
	fmt.Println(n)
}
./main.go:10:8: cannot use type Number outside a type constraint: interface contains type constraints

Further points, which Generics develops:

  • comparable is a general interface, usable only as a constraint; since Go 1.20 interface types satisfy it (with the run-time panic caveat above);

  • Go 1.25 removed the notion of core types from the spec, so the constraint rules are stated in terms of type sets and the operations they permit (Go 1.25 release notes);

  • the choice "interface or type parameter" is a design decision: use a type parameter when the same code must work for several types and the operation is the same (Sum, a Set[T comparable]); use an interface when the behaviour differs per type (Store);

  • generic methods (Go 1.27) let a method declare its own type parameters, but a method with type parameters can never implement an interface method (Go 1.27 release notes), and interface methods may not declare type parameters.

package main

import (
	"fmt"
	"strings"
)

// Set stores keys; the generic method Map takes its own type parameter (Go 1.27).
type Set struct{ items []string }

func (s *Set) Add(x string) { s.items = append(s.items, x) }

func (s *Set) Convert[T any](f func(string) T) []T {
	out := make([]T, 0, len(s.items))
	for _, x := range s.items {
		out = append(out, f(x))
	}
	return out
}

func main() {
	s := &Set{}
	s.Add("dune")
	s.Add("ubik")
	fmt.Println(s.Convert(strings.ToUpper))
	fmt.Println(s.Convert(func(x string) int { return len(x) }))
}
[DUNE UBIK]
[4 4]
package main

import "fmt"

type Mapper interface {
	Apply(x int) int
}

type doubler struct{}

func (doubler) Apply[T any](x T) T { return x }

func main() {
	var m Mapper = doubler{}
	fmt.Println(m.Apply(2))
}
./main.go:14:17: cannot use doubler{} (value of struct type doubler) as Mapper value in variable declaration: doubler does not implement Mapper (wrong type for method Apply)
		have Apply[T any](T) T
		want Apply(int) int

The have/want lines show that the generic method has the signature Apply[T any](T) T, which is not the signature Apply(int) int the interface requires. The way around is to instantiate by hand: declare a concrete method with a fixed type for the interface, and the generic one as an additional entry point.

Interfaces and reflection

Interface values are how reflect is entered: reflect.TypeOf(x) and reflect.ValueOf(x) take an any, and their results report the dynamic type. reflect.TypeFor[T]() (Go 1.22) gets a reflect.Type for a static type, which is the only way to describe an interface type itself, because a nil interface has no dynamic type; Type.Implements answers the same question as a type assertion but for types, not values. errors.AsType (Go 1.26) is the generic version of errors.As: it returns the value and a boolean and needs no pre-declared target variable.

package main

import (
	"errors"
	"fmt"
	"reflect"

	"example.com/shelf/catalog"
)

// errors.AsType[E error] requires E to implement error, so embed it.
type temporary interface {
	error
	Temporary() bool
}

type netErr struct{}

func (netErr) Error() string   { return "connection reset" }
func (netErr) Temporary() bool { return true }

func main() {
	// reflect: ask about interfaces at run time
	stringer := reflect.TypeFor[fmt.Stringer]()
	fmt.Println(stringer.Kind(), stringer.NumMethod(), stringer.Method(0).Name)
	fmt.Println(reflect.TypeFor[catalog.LoanState]().Implements(stringer))
	fmt.Println(reflect.TypeFor[catalog.Book]().Implements(stringer))
	fmt.Println(reflect.TypeFor[*catalog.MemStore]().Implements(reflect.TypeFor[catalog.Store]()))

	// errors.AsType (Go 1.26) with an interface type parameter
	err := fmt.Errorf("fetch: %w", netErr{})
	if t, ok := errors.AsType[temporary](err); ok {
		fmt.Println("temporary:", t.Temporary())
	}
	var tgt temporary // the older errors.As form takes a pointer to the target
	fmt.Println(errors.As(err, &tgt), tgt != nil)
}
interface 1 String
true
false
true
temporary: true
true true

Note that errors.AsType[E error] requires its type argument to implement error, so the interface temporary embeds error; the older errors.As takes a pointer to any interface type. More on reflect is in Reflection.

Vet and the compiler

The compiler catches the structural errors (missing method, wrong signature, pointer receiver, impossible assertion, impossible or duplicate switch case, a type-set interface used as a type). go vet adds checks that need a little more knowledge (the full list is in Formatting, vet, fix and Static Analysis):

Analyzer What it reports for interfaces

stdmethods

a method with a well-known name (ReadByte, WriteTo, Format, Scan, Seek, UnmarshalJSON, …​) and the wrong signature

ifaceassert

an assertion between two interfaces that no type could ever satisfy (conflicting method signatures)

errorsas

errors.As with a second argument that is not a non-nil pointer to a type implementing error or to an interface

copylocks

a value containing a sync.Mutex passed or stored by value, which breaks pointer-receiver types such as *MemStore

package main

import "fmt"

type number int

// The method name ReadByte is reserved by io.ByteReader: vet checks the signature.
func (n number) ReadByte() int { return int(n) }

func main() {
	fmt.Println(number(1).ReadByte())
}
./main.go:8:17: method ReadByte() int should have signature ReadByte() (byte, error)

Source: cmd/vet

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	_, err := os.Open("/nonexistent")
	var pe os.PathError
	fmt.Println(errors.As(err, pe)) // the target must be a pointer
}
./main.go:12:14: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
package main

import (
	"fmt"
	"io"
)

func main() {
	var r io.Reader
	// io.Reader.Read has a different signature from the asserted interface,
	// so no type can ever satisfy both.
	if x, ok := r.(interface{ Read() }); ok {
		fmt.Println(x)
	}
}
./main.go:12:17: impossible type assertion: no type can implement both io.Reader and interface{Read()} (conflicting types for Read method)

Analyzers from golang.org/x/tools outside the default go vet set (for example nilness, which can flag a typed nil returned as error) can be run with gopls or standalone drivers, and staticcheck adds more; see the same formatting-and-analysis page.

Pointers to interfaces and other pitfalls

  • A pointer to an interface is almost never what you want. A value of type *io.Reader has no methods: the pointer is not an interface, so the call does not compile. The FAQ’s When should I use a pointer to an interface? entry explains: pass the interface value itself (it already holds a pointer if the dynamic value is a pointer).

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func read(r *io.Reader) {
    	buf := make([]byte, 4)
    	n, _ := r.Read(buf) // a pointer to an interface has no methods
    	fmt.Println(n)
    }
    
    func main() {
    	var r io.Reader = strings.NewReader("dune")
    	read(&r)
    }
    ./main.go:11:12: r.Read undefined (type *io.Reader is pointer to interface, not interface)
  • An interface holds a copy. Assigning a struct to an interface copies it; later changes to the original are not seen through the interface, and methods with pointer receivers cannot be reached (they need the address of the original). Store a pointer when the interface must see the live object.

  • A method value on an interface also binds a copy for value receivers. s2.Speak in the earlier example did not see the later rename of d.

  • any swallows type information. Code full of any and type switches is a sign that an interface method or a generic function was missed. Prefer a named small interface or a type parameter.

  • An interface with too many methods is hard to implement and hard to fake. Split it; embed only when a consumer truly needs the union.

  • Do not return a concrete pointer as an interface in a conditional path (the typed-nil trap above), and do not compare err with a concrete *MyError variable: use errors.As or errors.Is.

  • Unexported methods seal an interface. interface{ isNode() } can only be implemented in its own package, which is how go/ast and database/sql keep closed sets of types; type switches over such a sealed interface are then exhaustive by convention, although the compiler does not check it.

  • Interface satisfaction ignores the package for exported names but not for unexported ones. Two packages that each declare an unexported close() method do not satisfy each other’s interfaces.

  • Methods from the embedded field of a non-pointer struct are in the struct’s method set only if the field’s receiver allows it. If S has a pointer-receiver method, then struct{ S } (a value) does not satisfy an interface needing it, while struct{ *S } and *struct{ S } do.

  • Concurrent use. An interface value is two words and is not updated atomically: two goroutines assigning to the same interface variable without synchronization is a data race that can tear the pair (see Memory Model and Data Races). atomic.Value and atomic.Pointer[T] exist for that case.

Comparing with other languages

Concept Go In other languages

Declaring satisfaction

none: structural, checked by the compiler

Java/C#: implements/:; Swift: protocol conformance; Rust: impl Trait for T; TypeScript: structural like Go

Who may add a conformance

only the package defining the type (methods must be in the same package)

Rust: the crate with the trait or the type; Swift and C#: extensions in other modules

Default method bodies

none; use embedding of a helper struct or a function

Java/C#/Swift/Rust: default or protocol-extension methods

Static dispatch

via type parameters (the call is resolved per instantiation of the gc shapes)

Rust monomorphization; C++ templates

Dynamic dispatch

an itab and an indirect call through the interface value

virtual calls; Rust dyn Trait vtable

Associated types or constants

none in interfaces

Rust, Swift: yes

Run-time type inspection

type assertions, type switches, reflect

instanceof/pattern matching, Any downcasts

Null

the nil interface (and the typed-nil trap)

null references; Rust has no null

The mapping "class, interface, inheritance" to "struct, interface, embedding" is spelled out in Design Philosophy and Coming from Other Languages.

Where the books are outdated or wrong

Bodner’s chapter 7 introduces interfaces, "accept interfaces, return structs", embedding in interfaces, the nil interface and the empty interface, type assertions and switches, function types as interface bridges and dependency injection; those parts remain right. The details that have moved on since the book (written for Go 1.15) are:

  • the book writes the empty interface as interface{}; since Go 1.18 any is the idiom and the code in this page uses it;

  • older advice that an interface may not embed two interfaces declaring the same method no longer applies: since Go 1.14 that is allowed when the signatures are identical;

  • the generics chapter (chapter 15) uses the draft "type list" syntax and the Go2Go playground; the shipped design uses interfaces as type sets with unions and ~T (Go 1.18), better inference (1.21), the cmp.Ordered constraint, and generic methods (1.27, which still cannot implement interface methods);

  • comparison of interface values and the comparable constraint changed in Go 1.20 (interfaces satisfy comparable);

  • errors.As with an interface target and errors.AsType (Go 1.26) are the current way to ask for a capability of an error in a chain, instead of a type assertion on the outermost error value;

  • reflect.TypeFor[T]() (Go 1.22) replaces reflect.TypeOf((*T)(nil)).Elem() for describing interface types.

Vivien’s chapter 8 covers interface types, implicit implementation, subtyping, interface embedding, the empty interface and type assertions. Beyond the same interface{} versus any change, its rule that embedded interfaces may not overlap predates Go 1.14, and chapter 1 presents Stringer as if it were built in (it is fmt.Stringer; the predeclared interfaces are error, any and comparable). Go has no subtyping between struct types, only assignability to interface types, which is worth keeping in mind when reading the book’s "subtype" wording. The error coverage in Vivien’s chapters 1 and 5 also needs the typed-nil caveat explained on this page.

Version notes

  • Go 1.0: the error interface; implicit interface satisfaction since the first release.

  • Go 1.14: overlapping method sets in embedded interfaces are allowed.

  • Go 1.18: any, type-set interfaces with unions and ~T, comparable; a basic interface can still be used as a type.

  • Go 1.20: interface types satisfy comparable; http.ResponseController.

  • Go 1.21: slices.SortFunc and relatives reduce the need for sort.Interface.

  • Go 1.22: reflect.TypeFor.

  • Go 1.25: core types removed from the specification.

  • Go 1.26: errors.AsType.

  • Go 1.27: generic methods (which cannot implement interface methods); math/rand/v2 gains a generic method.