{} The Go Reference

Types methods · Go · Intermediate

Interfaces

Implicit satisfaction and structural typing, the (type,value) pair and dynamic dispatch, method sets, any and type switches, composition, stdlib interfaces, design, and the nil trap.

Types methods Intermediate ⏱ 11 min read Complete

🔌 Analogy

An interface is a wall socket. It promises a shape — two prongs and a voltage — not a brand. Any appliance that fits plugs in. Go never asks a type “do you declare yourself a Plug?”; it just checks “do you have the right prongs?” That check is the type’s method set. This one idea — describe behavior, not identity — is the backbone of idiomatic Go design.

What an interface is

An interface is a set of method signatures — a named contract of behavior. A type satisfies it by having those methods; there is no implements keyword and no explicit link:

type Stringer interface {
	String() string
}

type Point struct{ X, Y int }

// Point now satisfies Stringer — automatically, no declaration
func (p Point) String() string {
	return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

This is structural typing (often called duck typing, checked at compile time): if it has the methods, it fits. The payoff is decoupling — you can satisfy interfaces you’ve never heard of, including fmt.Stringer and io.Writer from the standard library, without those packages knowing your type exists. A producer of a type and a consumer that needs a behavior never have to agree on a shared base class; they only have to agree on a method shape.

Polymorphism through an interface

Write a function that takes the interface, and every implementer flows through the same code — no switch, no inheritance hierarchy:

graph TD
SHAPE["Shape interface { Area() float64 }"]
CIRCLE["Circle"] -->|satisfies| SHAPE
RECT["Rectangle"] -->|satisfies| SHAPE
SHAPE --> FN["totalArea(shapes []Shape)"]
polymorphism.go — editable & runnable
package main

import (
"fmt"
"math"
)

// A small interface: one method.
type Shape interface {
Area() float64
}

type Circle struct{ R float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

type Rectangle struct{ W, H float64 }

func (r Rectangle) Area() float64 { return r.W * r.H }

// One function, any Shape — even types added later, in other packages.
func totalArea(shapes []Shape) float64 {
var sum float64
for _, s := range shapes {
	sum += s.Area() // dynamic dispatch: the concrete Area() is chosen at runtime
}
return sum
}

func main() {
shapes := []Shape{Circle{R: 2}, Rectangle{W: 3, H: 4}}
fmt.Printf("total area: %.2f\n", totalArea(shapes)) // 24.57
}

Under the hood: the (type, value) pair

An interface value isn’t magic — it’s a two-word structure: a dynamic type (a pointer to the concrete type’s method table) and a value (the data, or a pointer to it). Calling a method looks up the implementation in that table — dynamic dispatch:

graph LR
IV["interface value (Shape)"] --> T["type → *Circle's method table"]
IV --> V["value → Circle{R:2}"]
T --> M["Area() implementation"]

Three consequences fall out of this model:

  • Comparisons: two interface values are equal when their types and values are equal. An interface is nil only when both halves are nil (the trap below).
  • Boxing: storing a concrete value in an interface may copy it to the heap so the pointer is stable — a small allocation. In tight loops this can matter.
  • Dispatch cost: an interface call is an indirect call through the table, slightly more expensive than a direct call and not inlinable. Real, but rarely your bottleneck.

Method sets decide who satisfies

Whether a type satisfies an interface depends on its method set, and that differs for T versus *T. The rule: the method set of *T includes both value- and pointer-receiver methods; the method set of T includes only value-receiver methods. So a pointer-receiver method means only a pointer satisfies the interface:

method-sets.go — editable & runnable
package main

import "fmt"

type Speaker interface{ Speak() string }

type Dog struct{ Name string }

// POINTER receiver — so only *Dog is in Speaker's method set
func (d *Dog) Speak() string { return d.Name + " says woof" }

func main() {
d := Dog{Name: "Rex"}

var s Speaker = &d // OK: *Dog has Speak
fmt.Println(s.Speak())

// var bad Speaker = d  // compile error:
//   Dog does not implement Speaker (Speak has pointer receiver)

// the practical rule: if any method needs a pointer receiver,
// pass pointers when you store the type in an interface.
fmt.Println("does value Dog satisfy Speaker?", false)
}

This is the single most common “why won’t this compile?” with interfaces — see methods for when to choose each receiver.

The empty interface and type switches

The empty interface interface{} — spelled any since Go 1.18 — has no methods, so every type satisfies it. It’s the escape hatch for “a value whose type I don’t know yet”: JSON decoding, fmt.Println’s arguments, generic containers before generics. You recover the concrete type with a type assertion or a type switch:

any.go — editable & runnable
package main

import "fmt"

// describe inspects a value of unknown type at runtime.
func describe(v any) string {
switch x := v.(type) { // x is typed inside each case
case nil:
	return "nil"
case int:
	return fmt.Sprintf("int: %d", x)
case string:
	return fmt.Sprintf("string of length %d", len(x))
case fmt.Stringer:
	return "Stringer: " + x.String() // interfaces work as cases too
default:
	return fmt.Sprintf("other type %T", x)
}
}

func main() {
for _, v := range []any{42, "go", 3.14, nil} {
	fmt.Println(describe(v))
}
}

Reach for any sparingly — it discards the type system. With Go 1.18+ generics, many old uses of any (containers, utility functions) are better expressed with type parameters; see type assertions for the full mechanics.

Composing small interfaces

Idiomatic Go favors small interfaces — the most powerful have a single method (io.Writer, io.Reader, fmt.Stringer, error). You build bigger contracts by embedding them:

// io.ReadWriter is literally two one-method interfaces glued together
type ReadWriter interface {
	Reader // Read(p []byte) (int, error)
	Writer // Write(p []byte) (int, error)
}

A type satisfies the composed interface exactly when it satisfies each embedded one. This is why so much of the standard library interoperates: a *os.File, a *bytes.Buffer, and a net.Conn are all io.ReadWriters, so anything written against that interface works with all of them. See embedding for the full mechanics.

Implementing a standard-library interface

The real power of structural typing: implement a stdlib interface and instantly plug into stdlib machinery. Give a type Len, Less, and Swap and sort.Sort will sort it:

sort-interface.go — editable & runnable
package main

import (
"fmt"
"sort"
)

// ByLen is []string with the three methods sort.Interface requires.
type ByLen []string

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

func main() {
words := ByLen{"banana", "kiwi", "fig", "apple"}
sort.Sort(words) // sort.Sort works on ANY sort.Interface — no generics needed
fmt.Println(words) // [fig kiwi apple banana]
}

The interfaces worth knowing by heart:

InterfaceMethod(s)You get
fmt.StringerString() stringcustom %v/Println formatting
errorError() stringa value usable as an error
io.Reader / io.WriterRead / Write([]byte)works with io.Copy, buffers, files, sockets
io.CloserClose() errordefer x.Close()
sort.InterfaceLen/Less/Swapsort.Sort
http.HandlerServeHTTP(w, r)mount on any router

Designing good interfaces

  • Keep them small. One or two methods compose and mock far better than a twelve-method “manager.” The bigger an interface, the fewer types satisfy it and the more it couples.
  • Define them at the consumer. An interface belongs next to the code that needs the behavior, not next to the type that provides it. The provider returns a concrete struct; the consumer declares the little interface it depends on.
  • Accept interfaces, return structs. Be liberal in what you take, concrete in what you give back — so callers keep the full method set and you keep freedom to extend.
  • Don’t add an interface “just in case.” A single implementation needs no interface; introduce one when a second implementation or a test seam actually appears. Premature interfaces (“interface pollution”) add indirection with no payoff.

🧪 Interfaces are the seam for testing

Because satisfaction is implicit, a small interface is the natural place to swap a real dependency for a fake. Have your service depend on a Store interface with the two methods it uses; pass the real database in main and an in-memory fake in tests — no mocking framework required. This is the same idea as dependency injection, and it’s why “depend on a narrow interface” pays off the moment you write a test.

The nil-interface trap

⚠️ A typed nil is not a nil interface

An interface value is a (type, value) pair, so it equals nil only when both halves are nil. Store a nil *T in an interface and the type slot is filled — the interface is not nil:

var p *MyError = nil
var err error = p       // err holds (*MyError, nil)
fmt.Println(err == nil) // false! — the famous typed-nil bug

This bites hardest when a function declares var err *MyError, maybe-assigns it, and returns it as error: callers see a non-nil error even on the success path. Fix: return the error interface type directly and return nil explicitly on success — never return a concrete pointer type for errors.

🐞 Fix the bug

The most famous interface gotcha in Go: a typed nil inside an interface is not nil. Valid input still takes the error branch here. Edit until Run & check matches.

🐞 typed-nil.go — fix the bug

validate returns a nil *validationError, but the caller's err != nil check still fires — the interface holds a type, so it isn't nil. Make valid input report as valid.

Expected output
valid input
package main

import "fmt"

type validationError struct{ msg string }

func (e *validationError) Error() string { return e.msg }

func validate(input string) error {
var err *validationError
if input == "" {
	err = &validationError{"input is empty"}
}
return err
}

func main() {
if err := validate("gopher"); err != nil {
	fmt.Println("failed:", err)
	return
}
fmt.Println("valid input")
}

Next: how types borrow behavior by embedding others — struct & interface embedding.

Check your understanding

Score: 0 / 5

1. How does a type declare that it implements an interface in Go?

Go uses structural typing: any type with the required method set satisfies the interface automatically, with no declaration. This lets you satisfy interfaces you didn't know existed, including ones from other packages.

2. What two things does an interface value hold?

An interface value is a (type, value) pair. The type half points at the concrete type's method table (used for dynamic dispatch); the value half holds the data or a pointer to it. Both halves being nil is what makes the interface == nil.

3. A method has a POINTER receiver: `func (d *Dog) Speak()`. Which satisfies the Speaker interface?

The method set of *T includes both value- and pointer-receiver methods; the method set of T includes only value-receiver ones. So a pointer-receiver method means you must pass a pointer to satisfy the interface.

4. What is the idiomatic Go guidance for interfaces at API boundaries?

Accepting a small interface lets callers pass anything that fits; returning a concrete type gives them its full surface. And interfaces belong to the consumer — define the minimal interface you need next to the code that uses it.

5. An interface variable holds a *nil pointer* of a concrete type. Is the interface == nil?

An interface is nil only when BOTH its type and value are nil. A nil *T stored in an interface gives it a concrete type, so iface != nil. This is a classic bug, especially when returning a typed nil error.

Comments

Sign in with GitHub to join the discussion.