Interface Representation (eface / iface)
Interface Representation (eface / iface)
Overview
An interface value is not the concrete value alone. It is a small descriptor: type metadata + data pointer (with special cases). That design explains allocation on conversion, the famous nil-interface trap, and method-call cost.
Usage: Interfaces.
Diagram: eface vs iface
regions: any / empty interface | non-empty interface
flow:
[Conc]
|
v
[data pointer]
[itab: inter + _type + fun]
|
v
[Methods]
Two Runtime Shapes
| Kind | When | Conceptual fields |
|---|---|---|
eface |
Empty interface any / interface{} |
_type *rtype, data unsafe.Pointer |
iface |
Non-empty interface | tab *itab, data unsafe.Pointer |
itab caches the mapping from interface method set → concrete methods for a pair (interface type, concrete type). First conversion may populate itab; later calls reuse it.
iface
tab ---> itab { inter, _type, fun[0..n] method ptrs }
data ---> concrete value or pointer to it
Dynamic Type and Dynamic Value
var i any = 3
// dynamic type: int
// dynamic value: 3i == nil is true only when both type and value parts are unset. Assigning a typed nil sets the type part:
var p *int = nil
var i any = p
fmt.Println(i == nil) // false — type is *int, data is nilThis is the #1 production bug when returning error:
func f() error {
var err *MyError = nil
return err // returns non-nil error interface!
}
return nil // correct empty interfaceConversion and Escape
Boxing a non-pointer into an interface often allocates so data can point at a heap copy (escape analysis decides). Hot loops that convert to any for logging or reflection pay for it.
go build -gcflags='-m' .Look for x escapes to heap near interface conversions.
Method Calls
Calling i.M():
- Load itab (or eface type).
- Index method slot.
- Call with concrete receiver in
data.
Devirtualization / inlining may optimize some cases after SSA; do not assume every interface call is a virtual disaster — measure. Still, concrete types in hot paths are simpler for the compiler.
Type Assert and Type Switch
v, ok := i.(T) // checks dynamic type
switch v := i.(type) { ... }Failed assert without ok panics. Prefer comma-ok form at boundaries.
Comparable Interfaces
Interfaces are comparable if dynamic types are comparable. Comparing interfaces compares dynamic type identity then values. Maps with any keys require comparable dynamic types at runtime or panic.
Experiment
go mod init example
go run .package main
import (
"fmt"
"unsafe"
)
type Stringer interface {
String() string
}
type N int
func (n N) String() string { return fmt.Sprintf("N(%d)", n) }
func main() {
// nil trap
var p *N = nil
var s Stringer = p
var empty Stringer
fmt.Println("typed nil iface == nil?", s == nil)
fmt.Println("empty iface == nil?", empty == nil)
// eface shape (illustrative sizes; do not depend on layout in prod code)
var a any = 42
var b any = p
fmt.Printf("any int: type-ish non-nil data non-nil? %v\n", a != nil)
fmt.Printf("any nil *N != nil? %v\n", b != nil)
// method call via iface
s = N(7)
fmt.Println("method", s.String())
// assert
if n, ok := s.(N); ok {
fmt.Println("assert", n)
}
// show that interface is two words on 64-bit
fmt.Println("sizeof any", unsafe.Sizeof(a))
}Expected output (typical amd64/arm64):
typed nil iface == nil? false
empty iface == nil? true
any int: type-ish non-nil data non-nil? true
any nil *N != nil? true
method N(7)
assert 7
sizeof any 16
What to notice: Interface is two machine words; typed nil is not interface nil.
Try next: Benchmark fmt.Sprintf("%v", concrete) vs logging with concrete fields — interface conversion shows up in alloc profiles.