Skip to content

Interfaces

An interface is a named collection of method signatures. Any type that has all of those methods satisfies the interface — without saying so explicitly.

type Shape interface {
Area() float64
Perimeter() float64
}

Any type with an Area() float64 method and a Perimeter() float64 method automatically satisfies Shape.

This is the defining feature of Go interfaces. Compare:

LanguageHow you declare satisfaction
Java / C#class Circle implements Shape
GoJust have the right methods

The result is decoupled code. A type in package geometry can satisfy an interface in package renderer without either package knowing about the other. This makes Go interfaces composable across package boundaries without any import dependency on the interface definition.

Go’s standard library is built on tiny, focused interfaces:

type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

The Go proverb: “The bigger the interface, the weaker the abstraction.” A two-method interface is satisfied by far more types than a ten-method interface, making it dramatically more reusable.

An interface with no methods is satisfied by every type. Since Go 1.18 it has the alias any:

var v any = 42
v = "hello"
v = []int{1, 2, 3}

Use any sparingly — it surrenders type safety. The typical use cases are generic containers (before generics were added), JSON unmarshalling into unknown shapes, and variadic logging functions.

The idiomatic Go guideline:

  • Function parameters should be interfaces — accept the broadest type that covers what you actually call.
  • Return values should be concrete structs — callers get the full API of the real type; they can narrow it themselves.
// Good: accepts an interface, returns a concrete type
func NewReader(rd io.Reader) *bufio.Reader { ... }
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rect struct {
W, H float64
}
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
func describe(s Shape) {
fmt.Printf("area=%.2f perimeter=%.2f\n", s.Area(), s.Perimeter())
}
// MyErr demonstrates the typed-nil-in-interface trap.
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func mayFail(fail bool) error {
var p *MyErr // nil, but typed *MyErr
if fail {
p = &MyErr{msg: "boom"}
}
return p // always a non-nil error interface, even when p is nil
}
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rect{W: 4, H: 3},
}
for _, s := range shapes {
describe(s)
}
err := mayFail(false)
fmt.Println(err == nil) // false — the interface holds a typed nil *MyErr, not a true nil
}

An interface value is two words: a pointer to an itab (type + method table) and a pointer to the data.

flowchart LR
  subgraph Iface["interface value (2 words)"]
    T["*itab"]
    D["data"]
  end
  T --> ITAB["itab
 type: Circle
 methods: [Area, Perimeter]"]
  D --> VAL["Circle{Radius: 5}"]
  ITAB -.dynamic dispatch.-> CALL["itab.fun[0](data) → Circle.Area(data)"]
An interface value holds a type/method table pointer and a data pointer — calling a method is an indirect call through the itab

You can check that a type satisfies an interface at compile time, without ever creating a value, using a blank assignment:

var _ Shape = (*Circle)(nil)

This line only compiles if *Circle has every method Shape requires. It costs nothing at runtime and catches a missing or misspelled method the moment you write it, instead of at the first call site.

A nil pointer inside an interface is not nil

Section titled “A nil pointer inside an interface is not nil”

An interface value is (type, data). It is only == nil when both words are nil. A function that declares a *T result but returns it through an error-typed return boxes a non-nil type descriptor around a nil data pointer — the interface itself is never nil, even though the pointer inside it is:

type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func f(fail bool) error {
var p *MyErr // nil, but typed *MyErr
if fail {
p = &MyErr{msg: "boom"}
}
return p // always returns a non-nil error interface, even when p is nil
}

Running this on Go 1.26.5:

<nil>
false
*main.MyErr

fmt.Println(err) prints <nil> — printing dereferences enough to show the pointer is nil — but err == nil is false, and %T confirms the dynamic type is *main.MyErr. The interface variable carries a type descriptor the moment you assign a typed nil pointer to it, and that type descriptor alone is enough to make the comparison fail. The fix is to return a literal nil when there is no error, never a typed nil pointer through an interface-typed return.

Storing a concrete value in an interface variable is called boxing. Whether it allocates on the heap depends on what the compiler can prove. Compiling the shapes example above with -gcflags=-m:

./main.go:11:6: can inline Circle.Area
./main.go:16:20: devirtualizing s.Area to Circle
./main.go:16:20: inlining call to Circle.Area
./main.go:15:16: c does not escape
./main.go:16:20: ~r0 escapes to heap

When only one concrete type ever flows into s Shape in that function, the compiler devirtualizes the call — it knows statically that s.Area means Circle.Area, inlines it, and c itself never escapes. But storing two different concrete types into the same interface-typed slice defeats that:

./main.go:19:20: c escapes to heap
./main.go:19:27: Rect{...} escapes to heap

Once a []Shape can hold either a Circle or a Rect, the compiler can no longer know which Area to call without going through the itab at runtime, so both values are boxed onto the heap. This is the real cost behind “interfaces aren’t free” — not the interface value itself (still two words on the stack), but the boxed data it points to when static devirtualization is not possible.

Two interface values are comparable with == only if their dynamic types are comparable. The compiler cannot know the dynamic type ahead of time when both sides are any, so it always allows the ==, and defers the real check to runtime:

var a, b any
a = []int{1, 2}
b = []int{1, 2}
fmt.Println(a == b)
panic: runtime error: comparing uncomparable type []int

Slices, maps, and functions are not comparable — storing one inside an interface does not change that. Comparing two interface values whose dynamic type is a struct containing a slice or map field panics the same way. This is a common source of a crash deep inside test-assertion helpers or map[any]any keys that looked fine until a slice-typed field showed up.

Define interfaces at the consumer, not the producer

Section titled “Define interfaces at the consumer, not the producer”

A common anti-pattern imported from Java/C# codebases is a producer package that defines a large interface next to its one concrete implementation — type UserServiceInterface interface { ... } alongside type UserService struct{ ... }, with every method of UserService copied onto the interface “for testability.” This is interface pollution: the interface adds a layer of indirection without decoupling anything, since there is still only one implementation, and every time UserService grows a method the interface must grow too.

The idiomatic Go shape is the opposite: the package that uses a dependency declares the smallest interface it needs, right where it uses it. The producer package exports only a concrete type. This is exactly what io.Copy does — its signature is func Copy(dst Writer, src Reader) (int64, error), defined in io itself, not by every type that happens to be copyable. A caller passing *os.File, *bytes.Buffer, or net.Conn never needs those types to “implement” anything on purpose; they satisfy io.Reader/io.Writer because they already have the right methods.

Two symptoms mean an interface should move to the consumer or disappear entirely:

  • The interface has exactly one implementation in the entire codebase, and no test ever swaps in a fake.
  • The interface’s method set is just “every exported method of the one struct that implements it” — a mechanical copy, not a deliberately small contract.
What you getBenefitCost
Implicit satisfactionDecoupled, no import dependencyHarder to discover which type implements which interface
Small interface (1–2 methods)Composable, easy to implementMay need several interfaces together
any / empty interfaceAccepts any typeGives up type safety entirely
Dynamic dispatchFlexibilityReal cost: an indirect call (~1–3 ns), lost inlining, and the value may escape to the heap when boxed
  • Interfaces are free. An interface adds one layer of indirection and a dynamic-dispatch cost.
  • A nil interface and an interface holding a nil pointer are the same thing. A nil interface is (nil, nil); an interface holding a nil pointer is (*T, nil) — the two are not equal.
  • The more methods an interface has, the better. The Go proverb says the opposite: “the bigger the interface, the weaker the abstraction.”
  • Implicit satisfaction means duck typing. Go checks interface satisfaction at compile time, not at runtime.

💡 Real-world example

io.Reader (one method): http.Request.Body, os.File, bytes.Buffer, and strings.Reader all implement it — composable across the ecosystem with zero import dependency between them.

Kubernetes’ runtime.Object interface (k8s.io/apimachinery/pkg/runtime) lets a single generic controller work with every Kubernetes API resource — decoupling through a small interface.

When a generated function returns an error (or any interface type), check for these before trusting it:

  • Does every return path use a bare nil, or does one path return a typed pointer variable that could itself be nil (var p *MyErr; ...; return p)? The second form makes err != nil true even on the “no error” path.
  • Is == used to compare two any-typed or interface-typed values? If either side’s dynamic type could be a slice, map, or func, this compiles fine and panics at runtime.
  • Does a package define an interface right next to its one and only implementation, with the interface’s methods mechanically mirroring the struct’s? That is interface pollution — check whether any other type or test actually needs the indirection.
  • Does the code assert or check for extra capabilities (Flusher, Hijacker, WriterTo) instead of assuming the concrete type? Skipping that check silently drops functionality wrapped types provide.
  • Is a function parameter typed as a concrete struct when only one or two methods are actually called on it? That is a missed opportunity for a small, consumer-defined interface — not a bug, but a design smell worth flagging.

🐛 Spot the bug

type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func validate(input string) error {
var problem *MyErr
if input == "" {
problem = &MyErr{msg: "empty input"}
}
return problem
}
func main() {
err := validate("ok")
if err != nil {
panic("validation failed: " + err.Error())
}
}
Show the answer

validate returns problem, a *MyErr variable, through an error-typed return — even on the success path where problem is nil. The returned error interface then holds (type: *MyErr, data: nil), which is never == nil, so err != nil is true even though input was valid. main then calls err.Error() on that nil *MyErr receiver to read msg, which dereferences a nil pointer — the real panic is panic: runtime error: invalid memory address or nil pointer dereference, not the message text passed to panic(). The fix is to return a literal nil on the success path: if input == "" { return &MyErr{msg: "empty input"} }; return nil.

How does a Go type declare that it implements an interface?
What does this print?
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }

func f(fail bool) error {
    var p *MyErr
    if fail {
        p = &MyErr{msg: "boom"}
    }
    return p
}

err := f(false)
fmt.Println(err == nil)
What happens when this runs?
var a, b any
a = []int{1, 2}
b = []int{1, 2}
fmt.Println(a == b)
A package exports type UserServiceInterface interface { ... } with every method of its single UserService struct copied onto it, and nothing else in the codebase implements it or swaps in a fake. What is this called, and what should change?
What is the empty interface any (Go 1.18+) an alias for?
Your function needs to write bytes. Which parameter type is most idiomatic?