Interface Values Representation

Interface values are represented as a two-word pair, defined by a struct runtime.iface:

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

The first word tab is a pointer to an itable, which contains information about the type of the interface, the type of the data it points to and a virtual method table

The second word data is a pointer to the value (copy of the original) held by the interface. Values stored in interfaces might be arbitrarily large, but only one word is dedicated to holding the value in the interface structure, so the assignment allocates a chunk of memory on the heap and records the pointer in the data field

Value Boxing

When value of type T is assigned to an interface value:

  • If type T is a non-interface type, then the data field points to the copy of the T value. A copy is used because if the variable later changes, the pointer should have the old value, not the new one
  • If type T is also an interface type, then the data field points directly to the same value stored in Ts data field

Code example: Go Playground - Value Boxing

Example

Given:

type Stringer interface {
    String() string
}
 
type Binary uint64
func (i Binary) String() string {...}
func (i Binary) Get() uint64 {...}

On a 32-bit word size machine the interface Stringer storing the Binary is:

interface value representation|400

Itable

The itable is represented by a struct runtime.itab:

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
  • inter describes type information of the interface itself
  • _type describes the type information of the value an interface contains
  • fun is a variable-sized array of function pointers - the dispatch table of the interface

Type information about the interface data type is represented by an runtime.interfacetype which is an alias for abi.InterfaceType struct:

type InterfaceType struct {
	Type
	PkgPath Name // import path
	Methods []Imethod // sorted by hash
}

Note that, the itable corresponds to the interface type, not the dynamic type. In our example, it contains pointer only to String method, not Get

To check a type of the actual value an interface points to, compiler generates the code equivalent to s.tab->_type

To call s.String(), compiler generates the code equivalent to s.tab->fun[0](s.data)
Note that, the function in itable is being passed the pointer, not the actual value. Thus the function pointer in our example is (*Binary).String not Binary.String

Computing the Itable

The itable gets computed during the assignment (conversion) of the value to the interface variable: s := any.(Stringer)

There is a compile-time generated type description struct for each concrete type like Binary
Among other metadata, the type description struct contains a list of methods implemented by that type

Similarly, there is compile-time generated abi.InterfaceType struct for each interface type like Stringer
It too contains a list of methods

At run-time the itable is computed by looking for each method listed in the interface type’s method table in the concrete type’s method table
It then caches it, so the itable is computed only once

Note that, both tables are sorted, so the mapping is found in instead of naive

Optimizations

If an interface has no methods, itable is dropped and the first word points at the type directly. That is, runtime.eface is used instead of runtime.iface:

type eface struct {
	_type *_type
	data unsafe.Pointer
}

In Russ Cox blog post it is stated that if the actual value fits in a single word, it is stored in the second word directly without indirection or heap allocation. However, this is no longer true, as it caused race conditions in concurrent GC and ambiguity about whether the data word holds a pointer or scalar (todo elaborate further). So interfaces always store the pointer in the data field

Heap Allocations and Escape Analysis

When a method is called via an interface value instead of directly through a struct, the compiler generally lacks knowledge of the method’s implementation at compile time. Consequently, escape analysis cannot confirm that the value doesn’t escape, leading to heap allocations (there are optimizations) even for scalar types like ints, floats, strings

In some cases, the compiler can prove the concrete type of the value stored in the interface and devirtualize a method call, avoiding heap allocations

The Go runtime has a special static array of the first 256 integers (0 to 255), and when it would normally have to allocate memory to store an integer on the heap as part of converting it to an interface, it first checks to see if it can just return a pointer to the appropriate element in the array instead

Putting a zero-width type, e.g. struct{}, in an interface value doesn’t allocate

Addressability

The concrete value stored in an interface is not addressable (it’s a copy), in the same way that a map element is not addressable

Therefore, when you call a method on an interface, it must either have an identical receiver type or it must be directly discernible from the concrete type:

  • Pointer and value receiver methods can be called with pointers and values respectively
  • Value receiver methods can be called with pointer values because they can be dereferenced first
  • Pointer receiver methods cannot be called with values

References