The Empty Interface
Understand the empty interface (interface{} and the any alias) in Go - how it works, how to store arbitrary values, and common pitfalls.
Every Go type that has methods can satisfy an interface. The empty interface is the one exception that flips that requirement entirely: it has zero methods. Because of that, literally every Go type — including built‑in types like int, string, bool, and every custom struct — satisfies it automatically. An empty interface value can hold a value of any type.
This single idea powers some of the most flexible patterns in Go, from the standard library’s fmt.Println to JSON decoding and beyond. It also introduces the most common alias in modern Go: the any keyword.
How the Empty Interface Works Internally
An empty interface variable in Go does not just magically accept anything. Under the hood, on a 64‑bit system, an interface{} value occupies 16 bytes of memory and stores exactly two pieces of information.
The first 8 bytes hold a pointer to type information. This pointer describes the concrete type that is currently stored — essentially an internal runtime structure that says “this is an int” or “this is a string”. The second 8 bytes hold a pointer to the actual data value. If the value itself fits in 8 bytes (like an int, a float64, or a small struct), the data pointer may point to a word that contains the value directly. If the value is larger, Go allocates memory on the heap and the pointer refers to that allocation.
A useful mental model is a labelled box: one part of the box says “this contains an int”, and the other part holds the integer 42. The box can be relabelled at any time when you assign a different type.
package main
import (
"fmt"
"reflect"
)
func main() {
var i interface{}
fmt.Println(reflect.TypeOf(i), reflect.ValueOf(i))
i = 42
fmt.Println(reflect.TypeOf(i), reflect.ValueOf(i))
i = "hello"
fmt.Println(reflect.TypeOf(i), reflect.ValueOf(i))
}
Running this prints:
<nil> <invalid reflect.Value>
int 42
string hello
Before any value is assigned, the type pointer is nil — the variable itself is a true nil interface. The first assignment to 42 changes both pointers: the type is now int, and the data points to the integer 42. The second assignment changes them again.
Correct Mental Check:
If you see output like int 42 and string hello after the assignments, the interface is behaving correctly. The concrete type and value are stored independently and swapped out on each assignment.
The any Alias
Before Go 1.18, the only way to write the empty interface was interface{}. With the introduction of generics in Go 1.18, the language added a pre‑declared identifier any that is simply an alias for interface{}. Both syntaxes mean exactly the same thing and are completely interchangeable.
var x any // identical to var x interface{}
x = true
x = 3.14
The any form is preferred in new code because it makes the intent clearer: “this variable accepts anything” reads more directly than the double braces of interface{}. However, you will still encounter interface{} in older Go code, the standard library, and documentation — the two forms are equivalent.
any in Generics vs. Empty Interface:
The identifier any is also used as a type constraint in generics, where it means “any type at all.” That is exactly the same semantic as the empty interface, so func foo[T any](v T) and func foo(v interface{}) both accept all types. The difference is that generics preserve the concrete type at compile time, while the empty interface erases it until you assert it back.
Storing Arbitrary Values
Because every type satisfies the empty interface, you can assign any Go value to an interface{} variable without explicit conversion. This is what makes it the universal container.
func main() {
var box interface{}
box = 42 // int
box = "hello" // string
box = struct {
Name string
}{Name: "Gopher"} // anonymous struct
box = []int{1, 2, 3} // slice
fmt.Printf("(%v, %T)\n", box, box)
}
The final call prints ([1 2 3], []int) — the interface remembers both the value and the concrete type you put inside it.
The standard library’s fmt.Println and friends rely on this exact mechanism. Their signatures accept ...interface{}, meaning you can pass any number of arguments of any type, and the printing logic uses reflection internally to figure out how to display each one.
Boxing Costs:
When you assign a value to an empty interface, Go may need to allocate memory on the heap even if the original value was on the stack. An int normally fits in a register, but wrapping it in interface{} creates the 16‑byte box with a data pointer. For a single value this overhead is negligible, but in performance‑critical hot paths — like marshalling thousands of objects per second — the cumulative allocation and garbage‑collection pressure can become noticeable.
Accessing the Underlying Value
Storing an arbitrary value is only half the story. To use that value as its original type — to add an integer or concatenate a string — you must retrieve it. Go requires an explicit type assertion, because the compiler no longer knows what is inside the box.
The safe way to assert is the “comma‑ok” idiom:
var box any = 42
if v, ok := box.(int); ok {
fmt.Println("Got an int:", v)
} else {
fmt.Println("box does not hold an int")
}
If box holds an int, the assertion succeeds, ok is true, and v is the integer 42. If box holds something else, ok is false and v is the zero value of int (0). No panic.
Panic from Unsafe Assertions:
A single‑return assertion like v := box.(string) panics if box does not contain a string. This is a common cause of runtime crashes when handling JSON responses or user‑supplied data. Always use the two‑value form unless you have already proven the type through a preceding check.
There are two other ways to recover the concrete type: a type switch and the reflect package. The pattern you choose depends on how many possible types you need to handle.
Common Use Cases
Several real‑world Go patterns would be impossible or extremely awkward without the empty interface.
- Variadic functions for printing and logging.
fmt.Print,log.Print, and similar functions accept...interface{}. That single signature handles integers, strings, structs, and custom types without overloading. - JSON decoding with unknown or partial structure. When the shape of incoming JSON is not known in advance, a field can be typed as
interface{}or a map value asmap[string]interface{}. The decoder fills in concrete Go values (string, float64, bool, nested maps and slices) automatically, and the application inspects them afterward. - Storing arbitrary data in structures. A struct field of type
interface{}can act as a universal holder — for example, a cache that stores different types of results, or a context value bag. - Pre‑generics generic programming. Before Go 1.18, the only way to write a function that worked on multiple types was to take
interface{}arguments and use type switches or reflection inside. Generics now provide a cleaner alternative, but the empty interface remains the foundation of many standard library APIs.
// Decoding arbitrary JSON into a map
func main() {
data := []byte(`{"name":"Gopher","age":12}`)
var result map[string]interface{}
json.Unmarshal(data, &result)
fmt.Println(result["name"], "is", result["age"])
}
The output is Gopher is 12. The age field comes back as a float64 because JSON numbers are decoded that way, so if you need an integer you must assert it.
JSON Numbers Are Float64:
When you decode arbitrary JSON into interface{}, all JSON numbers become float64, not int. Accessing result["age"].(int) will panic because the concrete type is float64. Always assert the correct type or use a switch to handle the actual decoded type.
Common Mistakes and Misconceptions
Two pitfalls appear frequently, even in experienced Go codebases.
Mistaking a nil concrete pointer inside an interface for a nil interface. An interface value is nil only when both its type pointer and its value pointer are nil. If you assign a typed nil pointer (like *MyStruct(nil)) to an interface, the interface holds a non‑nil type pointer and is therefore itself non‑nil.
var p *int = nil
var box interface{} = p
fmt.Println(box == nil) // prints false
This causes subtle bugs when checking errors or return values. A function returning an error interface that wraps a nil concrete pointer is a classic case. A nil check on the interface will not catch it, and the program proceeds as if no error occurred when there actually was a problem.
Forgetting that the empty interface removes all compile‑time safety. When you store a value in an interface{}, the compiler cannot catch type mismatches. A mis‑typed assertion only fails at runtime. This is not a reason to avoid the empty interface, but it means that every use of interface{} should be paired with defensive checks at the boundary where the value is pulled back out.
Summary
The empty interface is the mechanism that lets Go bridge static typing and the kind of flexibility normally associated with dynamic languages — without abandoning type information at runtime. Internally, it is a compact 16‑byte structure that pairs a type descriptor with a value pointer. The any alias (available since Go 1.18) makes the intent clearer but is identical to interface{}.
Every assignment to an empty interface boxes the value, and every retrieval requires an explicit type assertion. The comma‑ok idiom is the safe way to assert; the single‑return form panics on mismatch and should be avoided unless the type has been proven beforehand.