---
title: What is the empty interface
summary: The empty interface is a Go type that accepts any value, enabling generic storage and flexible function parameters.
lang: en
slug: what-is-the-empty-interface
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/what-is-the-empty-interface/
---

# What is the empty interface



## The universal placeholder

You are writing a configuration loader that reads a JSON file and processes each field. Some fields are strings, some are numbers, some are nested objects. You want a single function to accept any value and route it to the correct handler. Go's type system refuses to let you write a function that accepts "anything" without a declared contract. You need a type that acts as a universal placeholder. That type is the empty interface.

The empty interface is written as `interface{}` in older code and `any` in Go 1.18 and later. Both spellings mean exactly the same thing. The compiler treats them as identical aliases. The name comes from the interface definition syntax: a list of required methods. An empty list means zero requirements. If a type needs to implement zero methods to qualify, every single type in Go automatically qualifies. Integers, strings, structs, slices, functions, and even other interfaces all satisfy it.

Think of it like a universal shipping container. The port crane does not care whether the container holds electronics, furniture, or raw materials. It only cares that the container fits the standard frame. The empty interface does the same thing. It accepts any Go value, wraps it in a standard runtime container, and passes that container around. You lose the original shape until you explicitly ask for it back.

The empty interface is a bridge between strict typing and dynamic flexibility. Cross it with intention.

## How the empty interface works

Assigning a value to an empty interface does not copy the value into a new variable. It boxes it. Under the hood, an interface value is a two-word structure maintained by the runtime. The first word points to the type information, known as the type descriptor. The second word points to the actual data. When you assign `42` to a variable of type `any`, the runtime allocates space for an integer, stores `42` there, and updates the interface header to point to the `int` type descriptor and that memory address. When you reassign `"hello"`, the runtime updates both pointers. The old integer becomes unreachable and the garbage collector cleans it up.

This two-word layout is why interfaces have a small memory cost. Every interface value carries type metadata alongside the data pointer. The runtime uses that metadata to verify type assertions and to dispatch method calls when the interface contains methods. The empty interface is special because it carries zero method requirements, so the runtime skips method dispatch entirely. It only tracks the type and data pointers.

Convention aside: the Go community prefers `any` in new code for readability, but `interface{}` remains standard in the standard library and older packages. Both compile to identical machine code. Pick one style per project and stick with it. The compiler does not care, but your teammates will.

Know the shape of the box before you pack it.

## Assigning and unboxing values

Here is the simplest assignment pattern. Declare a variable with the empty interface type and assign values of completely different shapes.

```go
package main

import "fmt"

func main() {
    // any is an alias for interface{}. They are interchangeable.
    var box any
    box = 42
    fmt.Println(box)
    // Reassign to a string. The old integer is garbage collected.
    box = "hello"
    fmt.Println(box)
    // Reassign to a slice. The interface value now holds a slice header.
    box = []int{1, 2, 3}
    fmt.Println(box)
}
```

Retrieving the value requires unboxing. You must tell the compiler what type you expect. Go provides two tools for this. A type assertion checks the box and extracts the value if the types match. A type switch tests multiple types in sequence without repeating the variable name.

```go
package main

import "fmt"

func main() {
    var box any = "world"
    // Type assertion with the comma-ok idiom prevents panics.
    // It returns the value and a boolean indicating success.
    if str, ok := box.(string); ok {
        fmt.Println("Got string:", str)
    } else {
        fmt.Println("Not a string")
    }
    // Type switch handles multiple possible types cleanly.
    // The compiler verifies that each case uses a valid type.
    switch v := box.(type) {
    case string:
        fmt.Println("It is a string:", v)
    case int:
        fmt.Println("It is an int:", v)
    default:
        fmt.Println("Unknown type")
    }
}
```

The comma-ok idiom is the standard way to assert types safely. It returns the extracted value and a boolean. If the boolean is false, the value is the zero value of the requested type, and no panic occurs. The type switch is syntactic sugar that expands to a series of assertions, but it avoids repeating the variable name and guarantees exhaustive checking when you include a `default` case.

Unboxing is a contract. Verify it before you spend it.

## The runtime representation

Go distinguishes between two internal interface representations. The empty interface uses `eface` (empty face). Interfaces with methods use `iface`. The `eface` structure contains only the type pointer and the data pointer. The `iface` structure adds a table of method pointers so the runtime can call methods dynamically. When you use `any`, the compiler generates `eface` operations. These are slightly faster because the runtime skips method table lookups.

The data pointer behavior changes based on size. Small values that fit in a machine word, like `int`, `bool`, or pointers, are often stored directly in the data pointer without heap allocation. Larger values, like structs or slices, are copied to the heap, and the data pointer references that heap allocation. This means assigning a large struct to `any` triggers a heap allocation and a memory copy. The garbage collector must track that allocation. If you pass large structs through `any` in a tight loop, you will see allocation pressure and GC pauses.

This memory behavior explains why `any` is not a free lunch. It trades compile-time guarantees for runtime flexibility, and that flexibility costs memory and CPU cycles. The runtime must allocate, copy, and track types that the compiler could have otherwise optimized away.

Measure the cost before you generalize.

## Realistic usage patterns

Real code uses the empty interface when building generic containers or serialization pipelines. JSON parsers are the classic example. JSON objects map to `map[string]any` because values can be strings, numbers, booleans, arrays, or nested objects. You write a recursive walker that inspects each value, asserts its type, and processes it.

```go
package main

import "fmt"

// WalkJSON recursively prints the structure of a parsed JSON object.
// It accepts any type because JSON values are inherently heterogeneous.
func WalkJSON(key string, val any, indent string) {
    // Type switch determines how to handle the current value.
    // Each branch extracts the concrete type safely.
    switch v := val.(type) {
    case string:
        fmt.Printf("%s%s: %q (string)\n", indent, key, v)
    case float64:
        // JSON numbers decode to float64 by default.
        // We print them without unnecessary decimal places.
        fmt.Printf("%s%s: %.0f (number)\n", indent, key, v)
    case bool:
        fmt.Printf("%s%s: %t (bool)\n", indent, key, v)
    case []any:
        fmt.Printf("%s%s: array with %d items\n", indent, key, len(v))
        // Recurse into each array element with an incremented indent.
        for i, item := range v {
            WalkJSON(fmt.Sprintf("[%d]", i), item, indent+"  ")
        }
    case map[string]any:
        fmt.Printf("%s%s: object with %d keys\n", indent, key, len(v))
        // Recurse into each nested object key.
        for k, nested := range v {
            WalkJSON(k, nested, indent+"  ")
        }
    default:
        fmt.Printf("%s%s: %T (unknown)\n", indent, key, val)
    }
}
```

Another common pattern is an event bus or message queue where different handlers process different payloads. You define a single channel of type `chan any` and send structs of varying shapes into it. The receiver uses a type switch to route the event to the correct handler. This works well for debugging, logging, or plugin systems where the exact message types are not known at compile time.

The empty interface shines when the data shape is truly unknown. Force it when the shape is fixed, and you will pay in complexity.

## Pitfalls and runtime traps

The empty interface trades compile-time safety for flexibility. You can store anything, but you must verify the type before using it. A blind type assertion without the comma-ok idiom will panic at runtime if the types do not match. The runtime stops the program with `interface conversion: any is nil, not string` or `interface conversion: any is int, not string`. The compiler will not catch this mistake because the variable's static type is `any`.

Another trap is passing `nil`. A `nil` interface is different from an interface holding a `nil` pointer. If you assign a `nil` `*int` to an `any` variable, the interface is not `nil`. It contains a type descriptor for `*int` and a `nil` data pointer. Type assertions against it will succeed for `*int` but fail for `string`. The compiler rejects mismatched assertions with `invalid type assertion: x.(string) (non-interface type int on left)` if you try to assert on a concrete type, but runtime panics are the real danger with `any`.

Performance is the silent pitfall. Every assignment to `any` may allocate. Every type assertion checks the type descriptor at runtime. If you use `any` to bypass generics in a hot path, you will see measurable slowdowns. The compiler cannot inline or optimize through interface boundaries. It must generate dynamic dispatch code.

Convention aside: the community treats `any` as a last resort. If you find yourself asserting types in multiple places, you are likely fighting the type system. Generics or a specific interface usually provide a cleaner path. The `encoding/json` package uses `any` because JSON is inherently dynamic. Your business logic probably is not.

Trust the type system. Use `any` only when the data truly defies static typing.

## When to reach for any

Use `any` when you are building a serialization layer like JSON or YAML where the input schema is unknown at compile time. Use `any` when you need a temporary holding cell for heterogeneous data that will be processed by a single type switch. Use generics when you want type safety across a collection of uniform types. Use a specific interface when you only care about a subset of methods and want to enforce behavior. Use concrete types when the data shape is fixed and known. The empty interface is a tool for heterogeneity, not a replacement for proper typing.

Pick the constraint that matches your data. Less flexibility means fewer runtime surprises.

## Where to go next

- [How to Build an Extensible Application with Go](/en/how-to-build-an-extensible-application-with-go/)
- [What Are Build Tags in Go and How to Use Them](/en/what-are-build-tags-in-go-and-how-to-use-them/)
- [How to Use hashicorp/go-plugin for Plugin Systems](/en/how-to-use-hashicorpgo-plugin-for-plugin-systems/)