AlgoMaster Logo

Empty Interface

Last Updated: May 22, 2026

High Priority
10 min read

An interface with zero methods is the empty interface, written as interface{} or, since Go 1.18, as the built-in alias any. Because every type in Go has at least zero methods, every type satisfies it, which makes it the one slot in the type system that can hold a value of unknown type. This lesson covers what the empty interface is, why any exists as an alias, where it's useful, what it costs you, and where generics have replaced it in modern Go.

What the Empty Interface Is

A typical interface like Payable requires one or more methods. Drop the method requirement entirely and you get the empty interface:

The Anything type has an empty method set. A type satisfies an interface when its method set includes every method the interface requires. Empty means no requirements, so every concrete type satisfies it: int, string, float64, slices, maps, structs, pointers, channels, even other interfaces. One variable of type Anything can hold any of them, one at a time.

In real Go code you almost never write type Anything interface{} as a named type. You either use the literal interface{} (Go pre-1.18) or the built-in any (Go 1.18+). The same applies to function parameters, return types, slice element types, map value types: anywhere you want a slot that can hold any concrete value.

Three different concrete types, three variables of the same static type any. The compiler doesn't track which one holds what; that information lives at runtime, inside the interface value.

any Is interface{}

Go 1.18 introduced any as a predeclared alias for interface{}. They are exactly the same type. Anywhere you see one, you can write the other and the compiler treats them identically.

The values compare equal and the dynamic types match. There's no performance difference, no behavior difference, no edge case where one works and the other doesn't.

The reason any exists is readability. interface{} looks like a type with a body, and beginners often misread it as something more complicated than it is. any says exactly what it means: a value of any type. The Go team added it once generics arrived because type parameters use any as a constraint that means "no constraint", and seeing func Print[T any](v T) reads more naturally than func Print[T interface{}](v T).

The convention in modern Go is straightforward: write any in all new code. The old name still works, and is interface{} constantly in older code and in the standard library, but gofmt -r 'interface{} -> any' is a one-liner that converts a codebase, and most teams have made the switch.

Heterogeneous Containers

The most common reason to use any is a container that needs to hold values of different types. A slice of int can only hold ints. A slice of any can hold anything.

Consider an audit log for an online store. Each entry has a timestamp, an event name, and a payload, and the payload's shape depends on the event. A "product viewed" event has a product code. A "discount applied" event has a percentage. A "cart updated" event has the new total. The natural fit is a slice of entries where the payload is any.

Each entry's payload has a different concrete type (string, int, float64, bool), and they all fit into the same any field. The %v verb in fmt.Printf knows how to print any of them, so the loop doesn't need to inspect the type to display the entry.

The same pattern shows up with map[string]any when a record has dynamic or mixed-type fields. Configuration loaded from a file, JSON decoded without a schema, or a feature flag bag where each flag has its own value type: all of these naturally land in map[string]any.

The keys are all strings, but the values are a mix of types. Without any, you would need four separate maps, one per type, and you would lose the natural one-record-per-key layout.

How any Holds a Value

An interface value (empty or not) is represented at runtime as a pair: a pointer to type information and a pointer to the underlying value. People sometimes call this the "two-word representation". The first word says "what is this", the second says "where is it". When the interface is nil, both words are zero.

The diagram shows the shape of an any slot. The type descriptor identifies the concrete type that's currently stored, and the value pointer points to the actual data. When you write var x any = 42, the compiler builds an interface value whose type descriptor says "int" and whose value pointer points to a copy of 42.

That value pointer is where the cost lives. Assigning a small value to an any often forces the runtime to allocate a heap location for the value so the interface can hold a pointer to it. The literal 42 would normally live in a register or on the stack; storing it in an any boxes it into a tiny heap allocation. Reading the value back unboxes it.

Storing primitive values like int or float64 in any typically allocates on the heap and adds indirection on every read. Hot loops that push millions of values through any pay for it. When the values share one type, use a concrete slice ([]int) or generics. Reserve any for genuinely heterogeneous data.

The compiler optimizes a few common cases (small integers, certain pointer-shaped values), but the general rule stands: an any slot is two pointers wide and usually involves an allocation. This is one reason any lost its place as the default abstraction once generics arrived.

any in the Standard Library

The standard library uses any in a few well-known places, and recognizing the pattern helps you read existing code.

The most common case is fmt.Println, fmt.Printf, and friends, all of which take a variadic ...any:

fmt.Println accepts any number of values of any type. It uses runtime reflection on each argument's dynamic type to decide how to print it. This is why one call can mix strings, ints, and floats with no fuss. The price is reflection overhead, which is fine for log output and is one of the reasons fmt is slower than format-specific encoders like the ones in strconv.

The other big one is JSON decoding when you don't know the shape of the document up front. Decoding into map[string]any gives you a dynamic tree where every value is any and you walk it with type assertions or a type switch.

The JSON decoder doesn't know the schema, so it picks default Go types for each JSON kind: strings become string, numbers become float64, booleans become bool, objects become map[string]any, arrays become []any, and null becomes nil. Note that JSON 12 decodes as float64(12), not int(12), which is a common pitfall when they try to assert the field as int. We'll see how to read these values safely in the next two lessons on type assertions and type switches.

If you do know the schema, decode into a struct instead. The struct path is faster, type-safe, and gives you autocompletion. The map[string]any path is for cases where the shape genuinely varies between documents.

When You Can Use an any Value

With any: once you've stored a value in it, you can't do much with it directly. The compiler only knows it's "some value", not what type, so it won't let you call methods or do arithmetic on it.

To do anything beyond storing, printing with %v, or comparing for equality, you have to recover the concrete type. There are two ways: a type assertion, which pulls out one specific type, and a type switch, which checks several candidates in one place. We'll cover both in the next two lessons. For now, here's a quick taste so the picture is complete:

The x.(float64) is a type assertion: "give me the float64 inside, and a bool that says whether the assertion succeeded." Without that step, the multiplication price*1.1 couldn't compile, because the compiler doesn't know x is numeric.

That extra step is the real tax on any. Every value going in is one assignment; every value coming out is an assertion or a switch. Code that pushes everything through any ends up checking types everywhere, which is verbose and easy to get wrong.

Generics Replaced Many Uses of any

Before Go 1.18, any (then written as interface{}) was how you wrote functions that accepted multiple types. A generic Max function, a generic queue, a generic cache: they all leaned on interface{} and a pile of type assertions inside.

Generics changed that. A type parameter says "this function works for many types", but it keeps the type known at compile time, so no boxing, no assertions, no reflection.

Here's the difference for a small helper that returns the first element of a slice:

The generic first is strictly better when all elements share one type. The caller passes []float64 directly, gets back a float64, and never assert anything. The any version forces a []any (a different type from []float64, requiring an explicit copy or rebuilding) and returns an any (forcing a type assertion).

The rule of thumb now: if every value in the container shares a type, or if the same function operates on one type at a time, use generics. If the values genuinely have different types and need to coexist in one slot, any is still the standard tool. We'll cover generics in detail in a later section; for this lesson, the takeaway is that any is a smaller hammer than it used to be.

Where any Still Belongs

After everything above, any still has a clear job. Use it when the data really is heterogeneous and a single typed alternative doesn't fit.

A short list of cases where any is appropriate:

CaseWhy any fits
Logging and printf-style functionsThe whole point is to accept whatever you throw at them
JSON or YAML with unknown or dynamic shapeThe document's schema is decided at runtime
Generic event buses with mixed event typesOne channel carries many event shapes; consumers dispatch
Plugin or interpreter valuesStored values come from user code or external configuration
Configuration bags with mixed typesSome values are strings, some ints, some booleans, all keyed alike

When you find yourself reaching for any outside cases like these, pause and ask whether generics or an interface with a method would serve better. An interface with one or two methods captures behavior precisely and lets the compiler help. any discards that help.

A small event bus that mirrors the kind of code where any shines. Events of different shapes flow through one channel-like buffer, and a dispatcher decides what to do based on the dynamic type.

Three event types, none of which share a useful interface, all flowing through one slice and one function. The type switch is what makes it usable on the receiving end. This is the kind of design where any is useful: the events are genuinely different, but the routing layer treats them uniformly.

Comparing any Values

Two any values are equal when both their dynamic types and dynamic values are equal. If the dynamic types differ, they're not equal, even if the underlying values look similar.

a and b both hold an int value of 42, so they're equal. c holds an int64, a different type, so the comparison is false even though the numeric value matches. d holds a string, also different. Comparing any values follows the rule that the types must match exactly first, then the values.

There's a catch: comparing two any values where the dynamic type is not comparable (a slice, a map, or a function) panics at runtime. The compiler can't see the dynamic type, so it can't refuse at compile time. When comparing interface values that could hold a slice, guard the comparison.

Slices, maps, and functions aren't comparable with == in Go (except against nil). When they're wrapped in any, the comparison compiles, but the runtime check fails. This is rare in practice but because the error message is unfamiliar the first time you see it.

Equality on any involves reading the type descriptor and then comparing values, which is slower than comparing two concrete values directly. For tight loops that compare many values, work with the concrete type if you can.

What You Lose When You Use any

The list of trade-offs is worth stating directly so the choice is clear.

  • Type safety at compile time. A []any accepts any value, so the compiler can't tell you that pushing a bool where you meant a float64 is a bug. The mismatch surfaces at runtime, often from a failed type assertion or a panic.
  • Performance. Boxing small values into interface form is an allocation; reading them back is indirection plus a type check. None of this matters in cold paths, all of it matters in hot ones.
  • Readability for callers. A function that takes any says nothing about what it accepts. A function that takes a concrete type or an interface with a method tells the caller what's expected without reading the body.
  • Tooling support. Autocomplete, jump-to-definition, and static analyzers work on the static type. With any, there's nothing useful to jump to: every operation on the value has to wait until it's recovered to a concrete type.

These costs are why most Go style guides advise against any in public APIs unless the API has no choice. If you're designing a function, prefer a concrete type or a small interface. Reserve any for the cases where the data truly has no shared shape.