Interface Internals — Specification¶
Spec note: Go's language specification defines interface types and their semantics, but the memory layout of interface values (
iface,eface,itab) is implementation-defined. The text below cites the spec for behaviour and the official runtime sources for layout.Spec: https://go.dev/ref/spec#Interface_types Runtime sources:
runtime/iface.go,runtime/runtime2.go,runtime/type.go,internal/abi/iface.go.
Table of Contents¶
- Spec references
- Defined behaviour the spec guarantees
- Implementation contract from runtime/iface.go
- Layout in runtime/runtime2.go
- Type identity and the _type descriptor
- Comparison rules — formal
- Conversion semantics
- Type assertion semantics
- Reflection contract
- Version history of internals
- Compliance checklist
1. Spec references¶
Interface types¶
An interface type defines a type set. A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface.
— https://go.dev/ref/spec#Interface_types
Static and dynamic type¶
A variable of interface type also has a distinct dynamic type, which is the non-interface type of the value assigned to the variable at run time (unless the value is the predeclared identifier nil, which has no type).
— https://go.dev/ref/spec#Variables
Type assertions¶
For an expression x of interface type, but not a type parameter, and a type T, the primary expression
x.(T)asserts that x is not nil and that the value stored in x is of type T.— https://go.dev/ref/spec#Type_assertions
Comparison operators¶
Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable.
— https://go.dev/ref/spec#Comparison_operators
Nil¶
The nil identifier is the predeclared zero value for pointer, channel, function, interface, map, or slice types.
— https://go.dev/ref/spec#Predeclared_identifiers
The spec does not describe the two-word layout. That is an implementation detail in the runtime sources.
2. Defined behaviour the spec guarantees¶
| Property | Guaranteed by spec |
|---|---|
| Two-word memory layout | NO — implementation defined |
Equality of ==-comparable interface values | YES |
Panic on == for uncomparable dynamic type | YES |
| Type assertion is a runtime check | YES |
| Comma-ok form does not panic | YES |
nil interface == nil | YES |
| Typed-nil != nil | YES (follows from the spec definition) |
| Method dispatch through interface | YES (semantics) — implementation via itab is runtime-defined |
So the layout you study in this section is reliable on all current Go releases but is not a portable language guarantee.
3. Implementation contract from runtime/iface.go¶
The Go runtime, since 1.4, defines and maintains:
// runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr // variable length
}
runtime/iface.go exposes (internal) helpers:
getitab(inter *interfacetype, typ *_type, canfail bool) *itab— global cache lookup + lazy build.convT*(...)— boxing helpers per kind (convT64,convTstring,convTslice,convT16,convT32,convT).assertE2I,assertI2I,assertE2I2,assertI2I2— interface-target assertions.panicdottypeI,panicdottypeE— panic builders for failingx.(T)(non-comma-ok).
These names are stable enough for runtime authors but not part of the public API. Tools like delve, pprof, and gdb know about them.
4. Layout in runtime/runtime2.go¶
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod // sorted alphabetically by method name
}
type imethod struct {
name nameOff
ityp typeOff
}
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8 // see runtime/typekind.go
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
Key invariants:
_typeis unique per type (pointer equality == type identity).interfacetypeis unique per interface declaration._type.equalisnilfor slice, map, function types — encoding "uncomparable" right in the type descriptor.
5. Type identity and the _type descriptor¶
The spec on type identity (https://go.dev/ref/spec#Type_identity) defines when two types are the "same". The runtime materialises this with a single *_type per identity. For named types this is straightforward; for structurally identical types declared in different packages, the runtime still gives them distinct _type pointers (consistent with the spec, since named types are not identical to their underlying types).
Generic types complicate this: each instantiation of List[int] shares one _type because the GCShape algorithm picks a representative shape per pointer-shape category. See internal/abi/type.go and the Go 1.18 generics implementation notes.
6. Comparison rules — formal¶
From the spec, expanded with implementation detail:
i == j iff (i is nil and j is nil)
or (i.tab == j.tab AND
(i.tab._type.equal != nil) AND
i.tab._type.equal(i.data, j.data))
If i.tab == j.tab AND i.tab._type.equal == nil → runtime panic.
For eface:
i == j iff (i._type == j._type AND
(i._type.equal != nil) AND
i._type.equal(i.data, j.data))
or (i._type == nil AND j._type == nil)
Comparing iface to eface (one is interface{}, other is typed): the runtime first checks both _types equal, then proceeds.
7. Conversion semantics¶
The spec (https://go.dev/ref/spec#Conversions) says a value of type T is assignable to interface I when T implements I. The runtime implements:
- Concrete
T→I: build anifaceheader. If the static(I, T)pair is known to the linker, use the precomputed*itab; else callgetitab. - Concrete
T→any: build anefaceheader. Box the value viaconvT*if not pointer-shaped. - Interface
I→J: rebuild the header. IfJisany, wrap directly; elsegetitab(J, i.tab._type). - Interface
I→T(concrete): this is a type assertion, not a conversion.
Boxing rule (per runtime/iface.go convT):
size := typ.size
if isDirectIface(typ) {
return *(*unsafe.Pointer)(v) // store value directly in data word
}
ptr := mallocgc(size, typ, true)
typedmemmove(typ, ptr, v)
return ptr
isDirectIface is true when typ.kind & kindDirectIface != 0, set for pointer, channel, map, func, slice-of-pointer-element single-pointer struct, etc.
8. Type assertion semantics¶
Per spec, x.(T) produces: - A panic if T is a non-interface type and x's dynamic type is not T. - A zero-value + false in comma-ok form.
Implementation:
| Source | Target | Helper |
|---|---|---|
iface | concrete | i.tab._type == *_type(T) compare; else panicdottypeI |
eface | concrete | i._type == *_type(T) compare; else panicdottypeE |
iface | interface J | assertI2I(J, i.tab._type) returns new itab; else panic |
eface | interface J | assertE2I(J, i._type) returns new itab; else panic |
Comma-ok variants (*2I2, etc.) return (itab, true) or (nil, false) instead of panicking. The compiler chooses the variant by the syntactic form (v, ok := i.(T) vs v := i.(T)).
9. Reflection contract¶
reflect.TypeOf(i any) Type reads eface._type. reflect.ValueOf(i any) Value reads both fields and packs them into a Value struct (defined in reflect/value.go):
v.Interface() rebuilds an eface header from the value's typ_ and ptr, allocating a heap copy if the original was inline.
reflect.Value.Pointer() returns the address of the underlying value when the kind is pointer-shaped (Ptr, UnsafePointer, Func, Chan, Map, Slice). For other kinds it panics — consistent with the layout (no pointer to return for an inline scalar).
10. Version history of internals¶
| Go Version | Change |
|---|---|
| 1.4 | iface, eface, itab formalised in runtime/runtime2.go. |
| 1.5 | Linker emits static itabs in .rodata. |
| 1.7 | _type reorganised; equal function pointer added (replacing per-kind switch). |
| 1.9 | convT2* family split for fast paths. |
| 1.13 | staticuint64s cache extends boxing fast paths for small ints. |
| 1.18 | Generics: GCShape stenciling; internal/abi introduced as the new home for type descriptors. |
| 1.21 | runtime.Pinner added; cleanup of conversion helper names. |
| 1.22 | Refactored getitab and assertX for register ABI. |
| 1.23 | Devirtualization improvements; method-value dispatch can sometimes inline. |
The two-word layout has been stable since 1.4. The itab.fun variable-length scheme is unchanged.
11. Compliance checklist¶
- Code does not depend on the order of
itab.funentries — that order matchesinterfacetype.mhdr(sorted by method name). - Code does not assume that two
*_typepointers for structurally identical types are equal — they are NOT for distinct named types. - Code does not retain raw
unsafe.Pointertoitabacross plugin reloads (plugins are not unloadable; the issue does not arise, but the assumption is documented). - Comparisons of interface values guarded against panic when dynamic type may be uncomparable (
reflect.DeepEqualor explicit type switch). - Functions returning
errorreturn literalnil, not a typed nil pointer. - Hot paths avoid converting non-pointer values to
anyunless required. - cgo boundaries do not pass interface headers; they pass handles or raw pointers with
runtime.Pinner. - Reflection-driven code calls
Interface()only when needed; repeated round-trips are profiled.
Spec quote — concluding text¶
Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
The two-word implementation makes this deterministic and cheap. The runtime sources (runtime/iface.go, runtime/runtime2.go) are authoritative beyond the spec for layout questions; they are open and well-commented and should be considered required reading for anyone debugging interface-value behaviour at depth.