0% completed
How Interface Values Work
On This Page
The Two Words
Storing a Value
Calling a Method
Comparing Interface Values
Key Takeaways
A pointer to an Order takes 8 bytes on amd64, and an Order takes 56. Store either one in a variable of type any, and that variable takes 16 bytes.
Output:
*Order: 8 Order: 56 any with Order: 16 any with *Order: 16
Sixteen bytes is 8 more than the pointer needs, and it is 40 fewer than the Order needs. The 56 bytes of the Order therefore cannot be inside the variable. They must be stored somewhere else.
What do those 16 bytes hold, what does storing a value cost, and what happens when code calls a method or uses == on an interface value?
The Two Words
An interface value is two units of memory the size of a pointer (machine words), which makes 16 bytes on amd64. The first word identifies the type of the value currently stored, and the second word locates the value itself. The runtime has two structs for this layout, declared in runtime/runtime2.go. This description is true of Go 1.27, and the details can change between releases.
type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type data unsafe.Pointer }
The struct eface represents the empty interface, which is any. It has no methods to call, so its first word points directly to a description of the type. The struct iface represents any interface with at least one method, such as error or io.Writer. Its first word points to a method table (an itab), declared in internal/abi/iface.go.
type ITab struct { Inter *InterfaceType Type *Type Hash uint32 Fun [1]uintptr // variable sized }
An itab belongs to one pair: one interface type and one concrete type. It records both types, and Fun lists the address of each method of the interface for that concrete type. The compiler emits the tables for the pairs that it can see in the program. When a pair appears only at run time, the function getitab in runtime/iface.go builds the table and keeps it in a cache, so each pair is built once.
The type stored in an interface value at a given moment is called the dynamic type, and the stored value is called the dynamic value. A variable declared as error always has error as its declared type (the static type), but its dynamic type can change with every assignment.
eface | iface | |
|---|---|---|
| Used for | any | Interfaces with methods |
| First word | Pointer to the type | Pointer to the itab |
| Second word | Data | Data |
| Can call methods through the first word | No | Yes |
Storing a Value
The data word is a pointer. Storing a pointer in an interface therefore places the pointer directly in that word. Storing a non-pointer value such as an Order copies the value to another location, and the data word points to that copy. The interface value and the original variable are then independent.
Output:
main.Order false *main.Order true
The first line reports the copy that was made before order.Paid changed, and the second reads through the pointer to the original. This is the same difference that Value and Pointer Semantics described, now with an interface as the holder.
Ordinary code cannot change the copy inside an interface value, because Go gives no way to take its address. An assignment can only replace it with a new value. This is also the reason a method with a pointer receiver is not available on the stored value. If IsPaid had a pointer receiver, the compiler would reject the first assignment:
./main.go:18:24: cannot use order (variable of struct type Order) as Payable value in variable declaration: Order does not implement Payable (method IsPaid has pointer receiver)
The copy usually goes to the heap. Stack, Heap, and Escape Analysis explained that cost and how to see it, and the measurement below applies it to the types here.
Output:
store Order: 1 store *Order: 0
Storing the pointer needs no new memory. Storing the value costs one allocation, because sink outlives the function and must refer to a stable copy. An interface value that stays inside one function can often avoid the allocation, since escape analysis applies here too.
Some non-pointer values also avoid it in Go 1.27. A map, a channel, a function, and a struct with one pointer field are each a single pointer in memory, so they are stored directly in the data word. For an integer from 0 to 255 or an empty string, the functions convT64 and convTstring in runtime/iface.go point the data word at fixed memory that already exists.
Calling a Method
A call through an interface must find the method at run time. The small function below shows the instructions that the compiler produces.
//go:noinline func check(p Payable) bool { return p.IsPaid() }
go build -o main main.go go tool objdump -s main.check ./main
The output, trimmed to the call and with the addresses removed:
MOVQ 0x18(AX), CX MOVQ BX, AX CALL CX
The two words of p arrive in the registers AX and BX. The first instruction reads the method address from byte 24 of the itab, which is where Fun starts. The second instruction makes the data word the receiver, and the third calls the address that was read. Compared with a direct call, this adds one memory read and makes the call go to an address held in a register (an indirect call).
The data word is always passed as the receiver. For a value type such as Order, the table therefore holds a small generated function, (*Order).IsPaid, which reads the Order through the pointer and then runs the method. Disassembling that function with go tool objdump shows it marked <autogenerated>.
The program below measures this cost. The inlined line calls a method whose body the compiler copies into the loop, and the direct line calls a method that the compiler is not allowed to inline. The interface line calls through a package variable, so the compiler cannot prove which type it holds.
One possible output (measured on one machine):
inlined 0.4 ns direct 1.1 ns interface 1.3 ns
On this machine, the interface call cost 0.2 ns more than a direct call that was not inlined. The numbers include the loop itself, so the differences between lines matter more than the totals. At that difference, one million calls per second would use 0.2 ms of each second, which is 0.02 percent of one CPU core.
The larger effects are indirect. The gap between inlined and the other two lines shows the first one, since a method called through an interface usually cannot be inlined. The second is that the compiler cannot determine which method body will run, so it must assume that pointer arguments escape. The program below counts the result for a method that takes an *Item.
Output:
direct: 0 interface: 1
The compiler removes this cost when it can prove the dynamic type. It then replaces the indirect call with a direct one (devirtualization). For var p Payable = &order followed by p.IsPaid() in the same function, go build -gcflags=-m reports this:
./main.go:19:22: devirtualizing p.IsPaid to *Order
An interface call rarely deserves attention on its own. It matters inside a loop that a CPU profile (a record of where a program spends its time) has already identified as slow.
Comparing Interface Values
Two interface values that are not nil are equal when their dynamic types are identical and their dynamic values are equal. The functions efaceeq and ifaceeq in runtime/alg.go perform the second check, after the comparison of the first words has shown that the types match. Each type's own rule then applies, so pointers compare by address and structs compare field by field.
Output:
same type, equal values: true pointers to equal values: false int64(1) and int(1): false
The compiler accepts == on two values of the same interface type, because their dynamic types are not known until the program runs. It rejects == only when neither operand can be assigned to the other's type, such as an io.Reader and an io.Writer. That leaves one failure that only appears at run time. If both values have the same uncomparable dynamic type, such as Order with its slice field, the comparison panics.
different types: false panic: runtime error: comparing uncomparable type main.Order
The first comparison does not panic, because the types differ and the values are never read. Whether == panics can therefore depend on the data. A test can easily miss this failure. A similar check runs when an interface value is used as a map key, as How Maps Work Inside showed with its slice key.
Key Takeaways
- An interface value is two words: one that identifies the dynamic type, and one that locates the dynamic value.
anyuseseface, whose first word points to the type, while an interface with methods usesiface, whose first word points to anitab.- Storing a pointer needs no allocation, while storing a non-pointer value copies it, usually to the heap.
- A method call through an interface reads the address from the
itaband calls it, which costs little but usually prevents inlining. - Interface values that are not nil are equal only when both the dynamic types and the dynamic values are equal.
- Comparing two values of the same uncomparable dynamic type panics at run time.
An interface value is a small fixed pair: what the type is, and where the value is. Almost every behavior of interfaces follows from that pair, including the copy on storing, the cost of a call, and the rules for ==. Knowing the pair also makes clear when an interface costs something real and when the cost is too small to matter in a working program.
Reading Progress
0%
On This Page
The Two Words
Storing a Value
Calling a Method
Comparing Interface Values
Key Takeaways