The universal box
You are building a configuration loader. The JSON file contains a mix of strings, numbers, booleans, and nested objects. You parse the file into a Go map. The map keys are strings, but the values could be anything. You need a type that says "I don't care what this is, just hold it."
That is the empty interface. In Go, interface{} is a type with zero methods. Every type in Go satisfies the requirement of having zero methods, so every type implements interface{}. It is the universal container. Go 1.18 added any as an alias for interface{} to make code easier to read. They are identical. The community prefers any in new code.
What the empty interface actually is
Think of interface{} as a cardboard box with no markings. You can put a shoe, a book, or a laptop inside. The box does not care about the contents. It just holds the object. When you take something out, you need to know what it is before you use it. The box doesn't tell you automatically. You have to check the label or try to use it and hope you're right.
In Go, the empty interface is defined as:
type interface{} interface{}
It has no method set. A type implements an interface if it has all the methods in the interface's method set. Since the empty interface has no methods, the condition is trivially true for every type. Integers, strings, structs, slices, functions, and even other interfaces all satisfy interface{}.
The any alias exists purely for readability. Before generics, code was littered with interface{}. In generic constraints, any looks cleaner. The compiler treats them as the same type. You can assign interface{} to any and vice versa without conversion.
Minimal example
Here's the simplest usage: store different types in the same variable and retrieve them.
package main
import "fmt"
func main() {
// interface{} accepts any value because it has no methods to satisfy.
var box interface{} = 42
fmt.Println(box) // prints: 42
// Reassign to a string. The variable type stays interface{}.
box = "hello"
fmt.Println(box) // prints: hello
// any is an alias for interface{} added in Go 1.18.
// They are identical; any is just easier to read.
var a any = box
fmt.Println(a) // prints: hello
}
The variable box holds an interface{} value. When you assign 42, Go wraps the integer inside the interface. When you assign "hello", Go replaces the contents with the string. The variable itself never changes type. It always holds an interface value.
Under the hood: type and data
An interface value is not just a pointer to data. It is a pair of words: a type pointer and a data pointer. The runtime stores the dynamic type and the dynamic value together.
When you assign an int to an interface{}, Go creates a pair. The type word points to the int type descriptor. The data word points to a copy of the integer value. If the value is small enough to fit in a word, Go may inline the data directly in the data word to save an allocation. This optimization happens automatically. You don't need to worry about it, but it explains why small values are cheap to box.
The pair structure has consequences. An interface value is nil only when both the type word and the data word are nil. If you put a nil pointer into an interface, the interface is not nil. It holds a type (the pointer type) and a nil data value. This distinction causes bugs.
Convention aside: Go follows the mantra "accept interfaces, return structs." Functions should accept interface{} or any when they need flexibility, but they should return concrete types when possible. Returning an interface hides the implementation and forces callers to do type assertions. Returning a struct gives the caller a known type with documented methods.
Realistic example: JSON parsing
The most common use of any is parsing JSON. The encoding/json package can unmarshal JSON into a map[string]any or []any when you don't know the structure ahead of time.
Here's a realistic helper that parses a JSON blob and extracts a value by key.
package main
import (
"encoding/json"
"fmt"
)
// getJSONValue parses JSON and returns the value for a given key.
// It returns any because the value could be a string, number, bool, or nested object.
func getJSONValue(data []byte, key string) (any, error) {
var result map[string]any
// Unmarshal into map[string]any to handle arbitrary JSON structures.
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
// Return the value directly. The caller must handle the type.
return result[key], nil
}
func main() {
jsonData := []byte(`{"name": "Alice", "age": 30, "active": true}`)
name, err := getJSONValue(jsonData, "name")
if err != nil {
fmt.Println("error:", err)
return
}
// Use a type assertion with the comma-ok idiom to check the type safely.
// The underscore discards the boolean result if you only care about the value.
if s, ok := name.(string); ok {
fmt.Println("Name:", s)
} else {
fmt.Println("Name is not a string")
}
}
The json.Unmarshal call populates the map with any values. Strings become string, numbers become float64, booleans become bool, arrays become []any, and objects become map[string]any. You get the data, but you lose compile-time type safety. You must use type assertions or type switches to work with the values.
Convention aside: The receiver name in methods is usually one or two letters matching the type. If you define a method on a struct that holds config, use (c *Config) Get(key string) any. Do not use (this *Config) or (self *Config). The Go community standard is short receiver names.
Pitfalls and runtime panics
The empty interface introduces runtime risks. The compiler cannot check types inside an interface. You can assert the wrong type and crash the program.
If you use a type assertion without checking the result, the program panics on failure. The compiler rejects this with a panic message like panic: interface conversion: interface {} is int, not string. The program stops immediately. Use the comma-ok idiom to avoid panics.
// Safe assertion returns the value and a boolean.
v, ok := val.(string)
if !ok {
// Handle the case where val is not a string.
}
The comma-ok idiom is the standard pattern. The boolean tells you if the assertion succeeded. If it failed, the value is the zero value of the target type, and the boolean is false. This lets you handle errors gracefully instead of crashing.
The nil interface trap is another common pitfall. An interface value is nil only when both its type and data are nil. If you return a nil pointer from a function that returns any, the interface is not nil. It holds the pointer type and a nil data value.
Here's the trap in action.
package main
import "fmt"
// getNil returns a nil pointer wrapped in an interface.
func getNil() any {
var p *int = nil
// The interface holds a type (*int) and a nil data pointer.
// The interface itself is NOT nil because it has a type.
return p
}
func main() {
val := getNil()
// This check fails. val is not nil; it holds a nil *int.
if val == nil {
fmt.Println("nil")
} else {
fmt.Println("not nil") // prints: not nil
}
}
The variable val is not nil. It is an interface holding a nil *int. To check if the underlying value is nil, you must assert the type first. This distinction matters when writing functions that return any. If you want to return a true nil interface, return nil directly, not a nil pointer wrapped in an interface.
Performance is another consideration. Boxing a value into an interface copies the value. If the value is a large struct, the copy is expensive. The interface also adds indirection. Accessing fields through an interface requires a type assertion or reflection, which is slower than direct access. Use any when you need flexibility, but prefer concrete types or generics when performance matters.
When to use any versus alternatives
The empty interface is powerful, but it is not always the right tool. Generics provide type safety without sacrificing flexibility. Concrete types provide the best performance and clarity. Choose based on your needs.
Use any when you are building a generic container like a JSON parser or a configuration map where values can be strings, numbers, or nested structures. Use generics when you need type safety and the type is known at the call site but varies across different calls. Use a concrete type when the type is fixed; the compiler can optimize better and you avoid runtime type assertions. Use a type switch when you receive an any value and need to branch logic based on the underlying type. Use the comma-ok idiom when you must check a type assertion without risking a panic.
Generics are the modern solution for most use cases that previously required any. If you are writing a function that works on any type but needs to enforce constraints, use a generic type parameter. If you are writing a function that truly accepts anything and does not care about the type, any is appropriate. The standard library uses any in places like fmt functions and json unmarshaling where the input is inherently heterogeneous.
The empty interface is a box. Generics are a mold. A box holds anything but tells you nothing. A mold shapes the data and guarantees its form. Pick the tool that matches your requirements.