Interfaces
An interface value in Go is a two-word struct: a pointer to a method table and a pointer to
the underlying data. That layout explains why type assertions are cheap, why interface
conversions sometimes allocate, and why interface{} has slightly different
bones from a method-bearing interface. This page walks through the runtime shapes and the
performance consequences that fall out of them.
What an interface value is
An interface value in Go is a 16-byte two-word struct: { *itab, *data }.
The first word is a pointer to an interface table, which carries the concrete
type and a method table. The second word is a pointer to the underlying value — or, for
a few word-sized cases, the value itself reinterpreted as a pointer.
The empty interface gets a slightly different shape called eface —
{ *_type, *data }. There's no method table because interface{}
has no methods to dispatch. Just enough to know what the concrete type is and where the
data lives.
// runtime/runtime2.go
type iface struct {
tab *itab // method table + types
data unsafe.Pointer // pointer to concrete value
}
type eface struct {
_type *_type // concrete type
data unsafe.Pointer
}The itab
An itab — interface table — exists once per
(interface_type, concrete_type) pair. The runtime builds them lazily on first
use and caches them in a global hash table keyed by that pair, so subsequent uses of the
same combination reuse the same itab.
Inside, an itab holds: a pointer to the interface type, a pointer to the
concrete type, a small type hash used for fast switching, and an array of function
pointers — the method table, with one entry per method on the interface, each filled in
with the address of the concrete type's implementation.
// runtime/runtime2.go (simplified)
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash, for type switches
_ [4]byte
fun [1]uintptr // variable-length method table
}A type assertion x.(T) is essentially "look up the itab for this interface,
check the concrete type pointer matches T". The slow part — building the
itab the first time — happens once. Every assertion after that is a pointer comparison.
Dynamic dispatch
Calling a method through an interface is two pointer dereferences: load the
itab from the interface value, load the right entry from its method table,
call indirect. Add the argument shuffle and you're at roughly 1.5× the cost of a direct
call on modern hardware — typically a few nanoseconds, dominated by the indirect branch
rather than the loads.
Since Go 1.21 the compiler will sometimes devirtualise interface calls when it can prove the concrete type at the call site — for example, when the interface variable is assigned a known concrete value within the same function. When devirtualisation fires, the indirect call becomes a direct call and may even be inlined. It's a quiet optimisation: rarely visible, occasionally important.
Type assertions and type switches
x.(T) compiles to a pointer comparison between the itab's concrete type
pointer and the target type. A type switch is just a series of these comparisons — the
compiler emits a small dispatch table keyed on the type hash to keep the work down when
there are many cases.
Both are cheap. A few nanoseconds per assertion on a hot path. The expensive part is what comes after — if the asserted type is one that doesn't fit in the data word and you go on to box it into another interface, that's where the allocation lives.
// Type switch — one comparison per case
switch v := x.(type) {
case *bytes.Buffer: // pointer compare against *bytes.Buffer's *_type
_ = v.Len()
case string: // pointer compare against string's *_type
_ = len(v)
case error: // assertion against an interface — itab lookup
_ = v.Error()
}Interface conversions and boxing
Assigning a concrete value to an interface variable is called boxing. If the
value fits in a single word and is itself pointer-shaped — actual pointers, channels,
maps, function values — the interface's data field can hold it directly,
no allocation needed. Anything else needs heap storage.
The runtime has a small family of helpers — runtime.convT,
runtime.convT16, convT32, convT64,
convTstring, convTslice — that allocate the right amount,
copy the value in, and return a pointer for the interface to point at. Each call shows
up in pprof as a malloc.
var x interface{} = 42 // calls runtime.convT64, allocates 8 bytes
var y interface{} = "hi" // calls runtime.convTstring, allocates a string header
// A pointer fits in the data word — no allocation:
buf := new(bytes.Buffer)
var z interface{} = buf // tab + data=buf, no mallocfmt.Printf is converted to interface{} at the call site. Pass
an int and you've allocated 8 bytes you didn't expect. In hot paths,
strconv directly into a pre-allocated buffer beats fmt by
more than the formatting cost.iface vs eface
The two interface shapes exist because the empty interface doesn't need a method table.
iface carries the full itab — interface type, concrete type,
and a method table to dispatch through. eface drops the itab and carries
only the concrete type pointer, since there's nothing to dispatch.
interface{} and any (added as an alias in Go 1.18) are the
same type, both eface-shaped. Conversion between a typed interface and
interface{} changes the first word: a typed interface holds an
*itab, an empty interface holds an *_type, and the runtime
extracts one from the other as needed.
Generics changed the calculus
Before Go 1.18, polymorphic code meant interfaces. Container types like sorted sets,
pools, and caches paid the boxing cost on every operation. With type parameters, the
compiler generates code specialised to a set of GC shapes (see
internals / generics) — pointer-shaped
arguments share one instantiation, but the data flows through as the concrete type, not
as interface{}.
For many use cases — generic containers, generic helpers like slices.Map
and maps.Keys — generics replace what would have been interface-based code
with concrete, monomorphised versions. No boxing, no dispatch, often inlined.
Interfaces remain the right tool when you actually need dynamic dispatch over an open
set of types; generics handle the cases where you wanted parametric polymorphism but
were settling for runtime dispatch.
Common pitfalls
- Accidental boxing in hot paths. Passing primitives through
interface{}— tofmt, to a logger that takes...any, to a generic-looking helper that's actuallyinterface{}-based — allocates per call. Allocations add up. - Nil interface that isn't nil. An interface value holding
(*T)(nil)has a non-nil itab and a nil data pointer; comparing it tonilreturns false. Returningerraserrorwhenerris a typed nil is the canonical source of surprised callers. - Type assertions on a nil interface.
x.(T)on a nil interface panics. Use the comma-ok form —v, ok := x.(T)— wherever the input might be nil. - interface{} where generics now apply. Container or helper code
written pre-1.18 with
interface{}can usually be ported to type parameters now, dropping allocations and gaining static type safety. - Empty interface as an untyped container. Functions that take
map[string]interface{}or[]interface{}to model "arbitrary data" push the cost of type-checking everywhere they're used. Define a real type if you can.
Production checklist
- Check allocations on interface-heavy code with
go build -gcflags=-mand benchmarks that report-benchmem. Unexpected mallocs are usually boxing. - Reach for generics before
interface{}when the polymorphism is parametric. Reserve interfaces for genuine dynamic dispatch over an open set of types. - Prefer concrete types for performance-critical paths. The dispatch cost is small per call but adds up when the call site sees millions of iterations.
- When a method exists on an interface "for testability", say so in the comment. It avoids the reader assuming polymorphism was the design intent.
- Use the comma-ok form for type assertions in any code that might see a nil interface or a different concrete type than expected. The panic-on-mismatch form belongs in code that's already verified the type.
Further reading
- Russ Cox — Go Data Structures: Interfaces — the canonical walk-through of the itab and the boxing rules, from 2009 but still accurate.
- runtime/iface.go
— itab construction, the global itab cache, and the
convTfamily. - runtime/runtime2.go
— the
iface,eface, anditabstruct definitions. - Keith Randall — Generics implementation in Go — how GC-shape stenciling interacts with interface-style dispatch.
- Internals — Generics — the other half of the polymorphism story.
- Internals — Escape analysis — what determines whether the boxing allocation hits the heap.