The Empty Interface (any)

In Go, the empty interface — spelled any since Go 1.18, and completely identical to the older interface{} — is the type that can hold a value of any type at all. Because it declares zero methods, every type in Go automatically satisfies it, which makes any the tool you reach for when a function or data structure genuinely cannot know its value’s type ahead of time: a generic container, a JSON decoder, or fmt.Println‘s own variadic parameter. Understanding exactly what any is under the hood — and where it can bite you — is essential to writing correct, idiomatic Go.

Overview: What the Empty Interface Is

Every interface value in Go is really a small, fixed-size struct with two words: a pointer to information about the value’s dynamic (concrete) type, and a pointer to (or copy of) the value itself. A normal interface, like io.Writer, restricts which types can be stored in it to those that implement its methods. The empty interface has no methods to implement, so the restriction disappears entirely — any type, including your own structs, maps, slices, and even other interface values, satisfies it trivially. This is why it is called “empty”: the method set is the empty set.

Since Go 1.18, the predeclared identifier any is a plain alias for interface{}. They are not two different types; they are the exact same type spelled two ways, and you can mix them freely. Modern Go style prefers any because it reads better and signals intent — “this can be anything” — without the visual noise of empty braces. You will still see interface{} constantly in older code and in the standard library’s older APIs, so recognize both.

Because interfaces are satisfied implicitly in Go (there is no implements keyword), assigning a value to an any variable never requires a declaration linking the two types — the compiler simply allows it, always, for every type. What happens next is called boxing: the concrete value is paired with a descriptor of its type and wrapped into that two-word interface representation. This boxing step is not free — it can cause a heap allocation — which matters if you reach for any in a hot loop instead of a concrete type or a generic type parameter.

It helps to think of any as a labeled box: the label records the type that went in, and the box holds the value. To get anything useful back out, you have to open the box and check the label — that’s what type assertions and type switches do, covered below.

Syntax

// declaring a variable of the empty interface type
var v any = someValue

// a function parameter that accepts a value of any type
func f(v any) {
	// ...
}

// type assertion: unsafe form, panics if the dynamic type is wrong
concrete := v.(SomeType)

// type assertion: safe "comma, ok" form, never panics
concrete, ok := v.(SomeType)

// type switch: branch on the dynamic type stored inside v
switch x := v.(type) {
case int:
	// x has type int in this branch
case string:
	// x has type string in this branch
default:
	// x keeps the type any
}
  • any — the predeclared alias for interface{}; a variable of this type can hold a value of any concrete type.
  • v.(SomeType) — a type assertion: extracts the value stored in v as SomeType, panicking if the dynamic type does not match.
  • v.(SomeType) with two return values — the “comma, ok” form: ok is true if the assertion succeeded, false otherwise, and it never panics.
  • switch x := v.(type) — a type switch: compares the dynamic type of v against each case in order and runs the first match; inside each case, x has that case’s specific type.

Examples

Example 1: A function that accepts anything

package main

import "fmt"

func describe(v any) {
	fmt.Printf("value=%v type=%T\n", v, v)
}

func main() {
	describe(42)
	describe("hello")
	describe(3.14)
	describe(true)
}

Output:

value=42 type=int
value=hello type=string
value=3.14 type=float64
value=true type=bool

The describe function’s parameter is any, so it accepts an int, a string, a float64, and a bool without complaint. Inside, %T asks fmt to print the dynamic type it finds boxed inside the interface value — proof that Go remembers exactly what was stored, even though the parameter’s static type is just “any type.”

Example 2: Unpacking values with a type switch

package main

import "fmt"

func classify(v any) string {
	switch x := v.(type) {
	case int:
		return fmt.Sprintf("int with value %d", x)
	case string:
		return fmt.Sprintf("string with length %d", len(x))
	case bool:
		return fmt.Sprintf("bool with value %t", x)
	case nil:
		return "nil value"
	default:
		return fmt.Sprintf("unknown type %T", x)
	}
}

func main() {
	values := []any{10, "golang", true, nil, 3.14}
	for _, v := range values {
		fmt.Println(classify(v))
	}
}

Output:

int with value 10
string with length 6
bool with value true
nil value
unknown type float64

A type switch is the idiomatic way to branch on the dynamic type stored inside an any. Go checks each case top to bottom and runs the first one whose type matches; inside that branch, the variable x is automatically re-typed, so len(x) works directly in the string case without any manual assertion. Notice the dedicated case nil, which matches only when the interface itself carries no type at all — and the default branch, which catches anything not explicitly listed (here, float64).

Example 3: Filtering a heterogeneous collection

package main

import "fmt"

func sumInts(items []any) int {
	total := 0
	for _, item := range items {
		if n, ok := item.(int); ok {
			total += n
		}
	}
	return total
}

func main() {
	mixed := []any{1, "two", 3, 4.5, 5}
	fmt.Println("sum of ints:", sumInts(mixed))

	record := map[string]any{
		"name":   "Ada",
		"age":    36,
		"active": true,
	}
	if name, ok := record["name"].(string); ok {
		fmt.Println("name:", name)
	}
	if age, ok := record["age"].(int); ok {
		fmt.Println("age:", age)
	}
}

Output:

sum of ints: 9
name: Ada
age: 36

This is the shape any takes in real programs: a []any or map[string]any holding mixed data, exactly like decoded JSON. sumInts walks the slice and uses the comma-ok assertion to safely skip anything that isn’t an int (the string and the float are ignored, only 1, 3, and 5 are summed). The record map shows the same pattern for pulling typed fields back out of a map[string]any, which is precisely what encoding/json produces when you decode into an any or map[string]any without a matching struct.

How It Works Step by Step

Consider var v any = 42, then later n, ok := v.(int):

  • Boxing. When 42 is assigned to v, Go builds an interface value: one word points to the runtime’s type descriptor for int, the other points to (or holds a copy of) the value 42. Storing a non-pointer value in an interface generally requires the value to live somewhere the data word can point to, which frequently means a heap allocation, unless the compiler’s escape analysis can prove it is safe to keep on the stack.
  • The zero value. An uninitialized any is nil — both the type word and the data word are empty, meaning “no type, no value.” This matters later: an interface is only truly nil when both words are empty.
  • Assertion. v.(int) compares the type word stored in v against the descriptor for int. If they match, the data word is handed back as an int. If they don’t match and you used the single-result form, the runtime calls panic. If you used the comma-ok form, ok is set to false and n is set to the zero value of int — no panic, ever.
  • Type switch. A type switch performs the same type-word comparison against every case, in source order, and executes the first branch that matches. This is why a type switch is really just a series of type assertions written more conveniently.
  • Unboxing is cheap; boxing is the expensive direction. Comparing type descriptors is a fast pointer comparison, so assertions and switches themselves are cheap. The allocation cost, if any, happens on the way in — when the concrete value is first boxed into the interface — not on the way out.

Common Mistakes

Mistake 1: Using the single-result type assertion on an unknown type

The single-result form trusts you completely — if you’re wrong about the dynamic type, it panics and crashes the program.

package main

import "fmt"

func main() {
	var v any = "hello"
	n := v.(int) // panics: interface conversion: interface {} is string, not int
	fmt.Println(n)
}

Unless you have already proven the type (for example inside a matching case of a type switch), always use the comma-ok form so a mismatch is just data, not a crash:

package main

import "fmt"

func main() {
	var v any = "hello"
	n, ok := v.(int)
	if !ok {
		fmt.Println("v is not an int")
		return
	}
	fmt.Println(n)
}

Output:

v is not an int

Mistake 2: Comparing two any values that hold uncomparable types

The == operator works on two interface values only if their dynamic types are themselves comparable. Slices, maps, and functions are not comparable, and Go only discovers this at runtime, which means it panics instead of failing to compile.

package main

import "fmt"

func main() {
	var a any = []int{1, 2, 3}
	var b any = []int{1, 2, 3}
	fmt.Println(a == b) // panics: comparing uncomparable type []int
}

When the underlying values might be slices, maps, or anything else that isn’t guaranteed comparable, use reflect.DeepEqual for structural comparison instead of ==:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a any = []int{1, 2, 3}
	var b any = []int{1, 2, 3}
	fmt.Println(reflect.DeepEqual(a, b))
}

Output:

true

Mistake 3: The typed-nil trap

An interface value is nil only when both its type word and its data word are empty. If you store a nil pointer of some concrete type inside an interface, the type word is filled in (it says “this is a *MyError“), so the interface itself is not nil, even though the pointer it holds is.

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string { return "my error" }

func doSomething() error {
	var err *MyError = nil
	return err
}

func main() {
	result := doSomething()
	fmt.Println(result == nil)
}

Output:

false

Even though err is a nil pointer, wrapping it in the error interface gives it a non-nil type word, so result == nil is surprisingly false. The fix is to return the interface’s own nil directly, rather than a nil-valued concrete type, whenever there truly is no error (or no value):

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string { return "my error" }

func doSomething() error {
	return nil
}

func main() {
	result := doSomething()
	fmt.Println(result == nil)
}

Output:

true

Best Practices

  • Prefer a concrete type, or a generic type parameter with a constraint, over any whenever the set of possible types is known at compile time — any throws away static type checking, so the compiler can no longer catch mistakes for you.
  • Always use the comma-ok form of a type assertion unless you have already established the type another way (for example, inside the matching case of a type switch).
  • Never compare two any values with == unless you are certain their dynamic types are comparable; reach for reflect.DeepEqual when slices, maps, or unknown types might be involved.
  • Avoid the typed-nil trap by returning the bare nil literal from functions whose return type is an interface, instead of a nil-valued concrete pointer.
  • Document, in a comment or the function name, exactly which concrete types a function accepting any expects — the compiler cannot enforce this for you, so your comments are the only contract.
  • Remember that boxing a value into any can allocate on the heap; in performance-sensitive code, measure before assuming it is free, and consider a concrete type or generics instead.
  • Reserve any for cases where the type is genuinely unknown until runtime — decoded JSON, reflection, generic logging — not as a default escape hatch from writing proper types.

Practice Exercises

  • Write a function sumFloats(items []any) float64 that sums only the float64 values found in a mixed slice, ignoring every other type, using the comma-ok type assertion. Test it on []any{1, 2.5, "skip", 3.5, true} and confirm it prints 6.
  • Write a function describeAll(items []any) []string that uses a type switch to build a slice of descriptions such as "int: 5" or "string: hello" for each element of an input slice, and print the results.
  • Reproduce the typed-nil example from Common Mistakes with your own interface and concrete pointer type, confirm it prints false, then fix it so it prints true by returning a bare nil instead.

Summary

  • any is the modern, predeclared alias for interface{}: an interface type with zero methods, which every type in Go satisfies implicitly.
  • Internally, an interface value is a two-word pair of (type descriptor, data); storing a concrete value in an any boxes it into that pair and can allocate on the heap.
  • Type assertions (v.(T) and the safe v, ok := x.(T) form) and type switches (switch v := x.(type)) unbox a value and dispatch on its dynamic type at runtime.
  • A truly nil interface has both words empty; an interface holding a nil pointer of some concrete type is not equal to nil — the classic typed-nil trap.
  • Use any deliberately, for genuinely dynamic data, and prefer concrete types or generics everywhere the shape of your data is known ahead of time.