These internals explain interface allocations, comparison panics and the famous non-nil nil error.
An interface value is two machine words:
- An empty interface (
any,efacein the runtime) holds a pointer to the dynamic type descriptor and a pointer to the data. - A non-empty interface (
iface) holds a pointer to an itab and the data pointer. The itab records the interface type, the concrete type, and a table of function pointers for the interface's methods, in a fixed order. Itabs are computed once and cached.
A method call through an interface loads the function pointer from the itab and makes an indirect call. It's cheap but can't be inlined, which is the real cost — the compiler can sometimes devirtualise calls when it can see the concrete type, and PGO does this for hot call sites.
The data word must be a pointer. Storing a pointer-shaped value (a pointer, map, channel or func) in an interface is free; storing anything else — an int, a struct, a string — normally boxes it: the value is copied to the heap. The runtime avoids allocation for zero-sized values, single-byte values and constants, but in hot code repeated boxing shows up in profiles.
Consequences:
- An interface equals
nilonly when its type word is nil. Avar p *MyErr = nilreturned aserrorhas a type word, soerr != nilis true. Return a literalnil. - Comparing interfaces compares the types, then the values with the type's
==. If the dynamic type is not comparable (a slice, map or func),==panics at run time, including when used as a map key. - Type assertion to a concrete type is a single pointer comparison; assertion to another interface needs an itab lookup.
Example
package main
import (
"fmt"
"testing"
)
type MyErr struct{}
func (*MyErr) Error() string { return "boom" }
func mayFail(fail bool) error {
var p *MyErr // typed nil pointer
if fail {
p = &MyErr{}
}
return p // WRONG: interface holds (*MyErr, nil)
}
var sink any
func main() {
fmt.Println(mayFail(false) != nil) // true — the typed-nil trap
n := 1000
allocs := testing.AllocsPerRun(100, func() { sink = n }) // int is boxed
fmt.Println("allocs per boxing:", allocs) // 1
defer func() { fmt.Println("recovered:", recover()) }()
var a, b any = []int{1}, []int{1}
fmt.Println(a == b) // panics: comparing uncomparable type []int
}Remember
- Interfaces are (itab or type, data pointer) pairs
- Non-pointer values are usually boxed on the heap when stored in an interface
- nil interface ≠ interface holding a nil pointer; uncomparable dynamic types panic on ==
Watch out for
- Declaring
var err *MyErrand returning it aserror— callers always see a non-nil error.