Fundamentals 11 min read

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.

TiPaiPai Technical Team
TiPaiPai Technical Team
TiPaiPai Technical Team
What Happens Inside Go’s Empty Interface? A Deep Dive into Memory Layout

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.

GoRuntimeMemory Layoutempty interface
TiPaiPai Technical Team
Written by

TiPaiPai Technical Team

At TiPaiPai, we focus on building engineering teams and culture, cultivating technical insights and practice, and fostering sharing, growth, and connection.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.