Interface Representation (eface / iface)

Updated

September 8, 2026

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: 3

i == 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 nil

This 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 interface

Conversion 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():

  1. Load itab (or eface type).
  2. Index method slot.
  3. 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.