What Happens Inside Go’s Empty Interface? A Deep Dive into Memory Layout
This article explores how Go implements the empty interface (interface{}) at the binary level, detailing stack frame allocation, compiler‑generated helper functions, pointer structures, and runtime optimizations that together reveal why an empty interface is essentially two consecutive pointers in memory.
Foreword
My first encounter with a Go executable was puzzling because its entry point differed from C/C++ binaries. Later, using Go for work sparked interest in its concise syntax and runtime behavior, leading to a deeper investigation of how Go programs execute.
Environment
OS: Ubuntu 20.04.2 LTS; 5.8.0-48-generic; x86_64 Go: go version go1.16.2 linux/amd64
Declaration
Different OS, CPU architecture, or Go version can change memory allocation, so results may vary across environments.
Conventions
interface{} denotes the empty interface.
In stack diagrams, the stack grows from top to bottom.
Each stack unit is 8 bytes (register size).
Stack frames shown do not include the return‑address area.
Empty Interface Type
The following diagram shows the empty interface in Go:
A simple HelloWorld program prints a string to standard output. The parameter of type interface{} can accept any concrete type, and a variable of this type can hold a reference to any value.
In Java, Object plays a similar role; in C, void * is a generic pointer, but it only accepts pointer values. Go’s interface{} can hold any value directly.
Code Listing
The code below defines PrintInterface to observe how an int is converted to interface{} and how it appears in memory.
Note: The compiler may inline small functions for efficiency. Adding //go:noinline prevents such optimizations, ensuring the function has its own stack frame.
Deep Dive into Memory
Compiled Code
The compiler renames functions; the original main and PrintInterface acquire fully‑qualified names that include the package name.
Part before the dot is the package name (e.g., main).
Part after the dot is the original function name.
Inspecting main
The compiled main function allocates a tiny stack frame (0x18 bytes, three registers). After the allocation, the stack looks like the following diagram:
① Put 123 into memory
movq $0x7b,(%rsp)
This instruction stores the integer 123 at the top of the stack, then calls runtime.convT64, a helper that converts a 64‑bit integer to a pointer to that integer.
The helper uses a static cache runtime.staticuint64s for integers 0‑255, avoiding heap allocation and reducing GC pressure.
Because 123 < 255, the function allocates a stack frame but does not store data there; the return value (a pointer) is written directly into the caller’s stack frame.
Both parameters and return values are passed via the stack rather than registers, which simplifies handling multiple return values.
③ Call PrintInterface
Before the call, two instructions store the type pointer (0x4a2140) on the stack, overwriting the previous 123 value.
lea 0xaacf(%rip),%rax # 0x4a2140 <type.*+0xa140> mov %rax,(%rsp)
The original integer remains reachable via the pointer saved earlier, so the overwrite is safe. The resulting stack frame is shown below:
Inspecting PrintInterface
The function first reads the two parameters from main ’s stack frame and stores them locally.
According to the calling convention, the first argument is the type pointer (0x4a2140) and the second is the data pointer (0x540AB8) that points to the actual integer 123.
First argument: 0x4a2140 (type information)
Second argument: 0x540AB8 (pointer to the integer value)
This confirms that an interface{} value consists of two consecutive pointers.
Source Code Reading
Inspecting reflect.TypeOf in the standard library reveals the same structure. The empty interface is represented by the internal struct emptyInterface (in internal/reflectlite/value.go) and by eface in runtime/runtime2.go.
The first pointer references the type descriptor; the second points to the actual data (e.g., the integer 123).
Learning Summary
After compilation, an interface{} value is stored as two consecutive pointers.
The first pointer references the type information; the second points to the concrete value.
Go caches integers in the range [0,256) in runtime.staticuint64s, reducing heap allocations and GC overhead.
Both function arguments and return values are passed via the stack rather than registers in the examined cases.
Further investigation is needed to determine when Go may use registers for parameter or return value passing.
TiPaiPai Technical Team
At TiPaiPai, we focus on building engineering teams and culture, cultivating technical insights and practice, and fostering sharing, growth, and connection.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
