---
title: 'The Empty Interface and `any`'
source: 'https://academia.sh/en/courses/go-type-system/empty-interface-and-any'
course: 'Interfaces, Errors and Generics'
language: en
updated: '2026-09-14T10:59:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the empty interface wants zero methods and is therefore satisfied by every type from every side'
    - 'Runs proof that the dynamic type does not change when the same value is carried by three separate static types'
    - 'Proves to the compiler that a value placed in the empty interface cannot be called directly and requires a type assertion'
    - "Measures that when a nil pointer is placed in the empty interface the interface itself is not nil, and ties this back to the Go Fundamentals course's zero-values lesson"
---

# The Empty Interface and `any`

The empty interface wants zero methods and every type satisfies zero methods: which type will arrive is never written in the source, and type information is carried inside the value. When the same value is carried by three separate static types, the dynamic type does not change; the cost is that anything placed in the value turns into a pair and cannot be called directly. Limiting measurement: when a nil pointer is placed in the empty interface, the interface itself is not nil.

The previous lesson read an interface value's dynamic type once — `main.File`, with `%T` —
and did not dwell on it. The empty interface is exactly that line grown large: an interface
wanting no method at all, defined not by a count of members but by the number zero. Since the
source never writes which type will be placed in this interface, someone somewhere has to
carry the placed type's information; the previous lesson's binding split already named that
place — **value**. This lesson grows that line.

Interfaces like the previous lesson's `Writer` and `Closer` wanted one method each; `Embedded`
wanted both together. The empty interface stands at the far end of that scale: it wants no
method at all. The relationship between how little an interface asks for and how many types
can satisfy it was measured with a count in the previous lesson; this lesson focuses on that
curve's zero point and measures what is gained and lost there.

## An Interface Wanting Zero Methods

In the previous lesson, as method count climbed from one to two to three, the number of the
same four types able to satisfy it fell from three to two to one. The empty interface is the
zero end of that same curve: `interface{}` wants no method at all, and a condition with no
methods is satisfied by every type — an empty set is a subset of every set. The language's own
type name is `any`, an alias for `interface{}`; this lesson calls the concept the **empty
interface**, its name in code `any`.

Because the empty interface wants no method, the source places no limit at all on which type
can go into it: `var v any = 42`, `var v any = "text"`, `var v any = Unit{}` all three compile,
because all three have zero methods to satisfy. But an interface wanting zero methods does not
mean the placed value keeps no information at all. Type information is not lost, only its
visibility changes — it stops being a type name readable from the source and turns into a
piece of information that has to be asked for at runtime.

Every `fmt.Println` call written throughout this lesson is already using this same mechanism:
the standard library's function takes its parameters as `...any`. Whether given an integer, a
string, or a struct, each is placed into the empty interface at the moment of the call and read
back inside `Println` with a query similar to `%T`. The empty interface is therefore not a
rare edge case — every output line in this lesson has already passed through it.

## The Same Value, Three Static Types

A value's dynamic type is independent of which static type carries that value. The following
measurement passes the same `Unit` value through three separate static types — its own
interface, then the empty interface, then the empty interface directly — reading the dynamic
type each time with `%T`.

```go
// same-value.go — the same value carried through three static types, the dynamic type does not change
package main

import "fmt"

type Unit struct{ Name string }

func (u Unit) Identity() string { return u.Name }

type Identifier interface{ Identity() string }

func main() {
	u := Unit{Name: "unit-42"}

	var t1 Identifier = u
	var t2 any = t1
	var t3 any = u

	fmt.Println("as Identifier         :", fmt.Sprintf("%T", t1))
	fmt.Println("after moving to any    :", fmt.Sprintf("%T", t2))
	fmt.Println("directly as any        :", fmt.Sprintf("%T", t3))
}
```

```
as Identifier         : main.Unit
after moving to any    : main.Unit
directly as any        : main.Unit
```

Three variables carry three separate static types — `Identifier`, then `any`, then `any` again
but arriving by a different path — and all three give the same answer: `main.Unit`. The move
from `t1` to `t2` is not a conversion, it is the same pair (type, value) binding to a new
variable once more; type information is not lost because there was never anything to lose at
any point — the interface value already carried this pair from the very start. `Identifier`'s
single method and the empty interface's zero methods both display the same underlying
structure; only the visible method count differs.

**IN4.** `t3 any = u` was written instead of `t3 := u` in order to also show a path where
`Unit` is placed directly into the empty interface — a third line was deliberately added to
tell apart the `t1` and `t2` chain, which passes through `Identifier`, from `t3`, which does
not.

## The Same Container, Four Separate Dynamic Types

The previous measurement passed a single value through three separate static types and showed
the dynamic type staying fixed. This measurement does the reverse: it places four separate
dynamic types into a single static type (`[]any`) and shows each keeping its own identity.

```go
// mixed.go — the same any slice carries four different dynamic types side by side
package main

import "fmt"

type Unit struct{ Name string }

func main() {
	values := []any{42, "text", Unit{Name: "x"}, 3.14}
	for _, v := range values {
		fmt.Printf("value=%v dynamic type=%T\n", v, v)
	}
	fmt.Println("slice's static type: []any, length:", len(values))
}
```

```
value=42 dynamic type=int
value=text dynamic type=string
value={x} dynamic type=main.Unit
value=3.14 dynamic type=float64
slice's static type: []any, length: 4
```

**IN5.** The array was deliberately chosen from four separate basic categories — an integer, a
string, a struct, a floating-point number — so that the demonstration is visible as the empty
interface's general rule and not a behavior specific to a single type family (numeric types
alone, for instance).

The slice itself carries a single static type: `[]any`. But its four elements carry four
separate dynamic types — `int`, `string`, `main.Unit`, `float64` — and the `range` loop never
mixes any of them up, because each element brings its own pair along with it. This mirrors the
previous measurement: there the same type stayed fixed across three static forms, here the
same static form carries four separate types. Together the two make clear where type
information really sits — not in the static type, but in the pair each value carries on its
own.

A heterogeneous container like this — holding values of different types in a single slice —
could only be built with the empty interface before generic data structures written with type
parameters existed. Where the two ways of writing this diverge will be measured in the course's
generics section; what matters here is only how the empty interface pulls this off: by storing
each element together with its own pair.

## The Cost: Being a Pair, Not Callable Directly

Carrying type information inside the value comes with a cost: a value placed in the empty
interface cannot be called **directly** through its own methods. Since `any` wants zero
methods, it offers zero methods — no method is defined on it, even if the placed value has
methods of its own. Calling it first requires returning to the real type (or an interface the
real type satisfies) with a type assertion.

This is the symmetric counterpart of the previous lesson's small-interface measurement. There,
as method count grew, the number of types able to satisfy it fell; here, when method count
drops to zero, access also drops to zero. The empty interface accepts every type because it
wants nothing, and because it wants nothing it also offers nothing — acceptance breadth and
direct access are inversely related. The compiler makes no guess here: as long as `v`'s static
type is `any`, it has no way of knowing which methods exist on `v`, because that information
can only be learned at runtime by looking at the value itself.

```go
// compiler.go — tests from inside the program whether a piece of source compiles
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}
```

This block is only written to disk; the real measurement, producing two pieces of source and
calling the compiler, follows below.

```go
// cost.go — a direct call over any is rejected, a call after a type assertion is valid
package main

import "fmt"

func main() {
	common := `package trial

type Unit struct{ Name string }

func (u Unit) Identity() string { return u.Name }

type Identifier interface{ Identity() string }

func F() string {
	var v any = Unit{Name: "x"}
`
	directCall := common + `	return v.Identity()
}
`
	assertedCall := common + `	return v.(Identifier).Identity()
}
`
	fmt.Println("direct call, over any        :", compiles(directCall))
	fmt.Println("call after a type assertion  :", compiles(assertedCall))
}
```

```
direct call, over any        : false
call after a type assertion  : true
```

`v.Identity()` does not compile, because `v`'s static type is `any` and `any`'s method set is
empty; the compiler does not even search for a method named `Identity` on `any`, because there
is no method defined there to search for. `v.(Identifier).Identity()` compiles, because the
type assertion first converts the value to `Identifier`, and a call can be made through
`Identifier`'s (and therefore `Identity`'s) method set. The cost is this: `any` accepts a
value from every type, but in exchange it closes off direct access to that value's own
behavior. The type assertion itself is the next lesson's topic; here only its **necessity** is
measured.

This rejection is not a gap, it is a deliberate boundary. When the compiler looks at the line
`v.Identity()`, all it has is `v`'s static type (`any`); which concrete value will sit there is
not yet known at compile time, it is determined only the first time it runs. The compiler does
not fill this gap by guessing "`Identity` is probably there" — such a guess would only blow up
at runtime, possibly in production, if it turned out wrong. Instead, the compiler never steps
outside the method set the static type offers; since that set is empty for the empty
interface, the call stops right there. The type assertion is the only way to close this gap
**explicitly**.

## Limiting Measurement: An Interface Carrying a Nil Pointer Is Not Nil

The strangest line involving the empty interface shows that carrying type information does not
come free. A pointer can be `nil`; when that same pointer is placed into the empty interface,
the interface itself does **not** become nil.

```go
// nil-interface.go — even if the pointer is nil, the interface carrying it is not nil
package main

import "fmt"

type Server struct{}

func main() {
	var s *Server
	var v1 any = s
	var v2 any

	fmt.Println("is the pointer itself nil     :", s == nil)
	fmt.Println("is v1 carrying the pointer nil:", v1 == nil)
	fmt.Println("is v2, given no value at all, nil:", v2 == nil)
}
```

```
is the pointer itself nil     : true
is v1 carrying the pointer nil: false
is v2, given no value at all, nil: true
```

The three lines were placed side by side deliberately. `s` really is `nil` — it points to no
`Server` instance. `v1` carries this `nil` pointer, but `v1 == nil` gives `false`: the
interface value is a pair, and this pair's type slot is filled — `*main.Server` sits there —
only the value slot is `nil`. The pair only counts as `nil` when **both** of its slots are
empty; `v2` never received any value, so both its type and value slots stay empty, and it
really is `nil`.

The type slot of the pair carrying genuinely separate information becomes even clearer when two
different pointer types are placed side by side.

```go
// two-nils.go — the nil values of two separate pointer types are not counted equal
package main

import "fmt"

type A struct{}
type B struct{}

func main() {
	var a *A
	var b *B
	var v1 any = a
	var v2 any = b

	fmt.Println("is v1 nil          :", v1 == nil)
	fmt.Println("is v2 nil          :", v2 == nil)
	fmt.Println("is v1 equal to v2  :", v1 == v2)
	fmt.Printf("v1 dynamic type=%T, v2 dynamic type=%T\n", v1, v2)
}
```

```
is v1 nil          : false
is v2 nil          : false
is v1 equal to v2  : false
v1 dynamic type=*main.A, v2 dynamic type=*main.B
```

`a` and `b` are both `nil` pointers and both, on the surface, point to a "empty" struct with no
fields. But `v1` and `v2` are not equal to each other, because an interface comparison first
compares the type slot, and `*main.A` and `*main.B` are not the same type. Even though both
value slots are `nil`, the two are not counted equal as pairs because their type slots differ.
This is one more proof that the type slot carries real information independent of the value
slot — not just "is it empty or not," it shows the "which type" question also enters the
comparison.

**IN6.** This is the direct consequence of the previous lesson's first claim: if eight of the
bindings are carried inside the value, a value being empty does not mean the pair is empty.
The Go Fundamentals course's Zero Values lesson measured that `nil` can be appended to a slice
but cannot be written to a `nil` map; this line is a third example from the same family — the
word `nil` stays the same, the meaning it carries changes from type to type.

The practical consequence of this line is this: if a function returns `error` (itself an
interface) and internally uses a concrete pointer type, the returned error value may **not** be
`nil` even if that pointer is `nil`. The caller asking `err == nil` gets `false`, because what
it is asking about is not the pointer itself but the pair carrying it. This trap's full
measurement will be done in the lesson devoted to error values; here only its origin is shown.

There is a reason for seeing this origin so early: an error is itself an interface, and
everything that is an interface is bound to the same pair rule. `error` is not the empty
interface — it wants the `Error() string` method — but what it shares with the empty interface
is exactly this lesson's subject: both are a pair carrying a type slot and a value slot. In the
empty interface this pair is easy to observe directly, because no method stands in the way; in
the error interface the same pair hides behind a method signature, but its behavior does not
change.

## Summary

- The empty interface wants zero methods and every type satisfies zero methods; `any` is this
  interface's name in code.
- When the same value is carried by three separate static types (its own interface, then
  `any`, then directly as `any`), the dynamic type does not change — type information is not
  lost, only its visibility changes.
- A value placed in the empty interface cannot be called directly; since `any`'s method set is
  empty, a call first requires a type assertion.
- When a nil pointer is placed in the empty interface, the interface itself is not nil — the
  pair is only counted nil when both its type and value slots are empty together.
- The `nil` values of two separate pointer types are not counted equal when placed in the
  empty interface; the comparison looks at the type slot before the value slot.

## Next Step

In this lesson, type information was accessed only with `%T`, for reading purposes alone — no
line ever recovered `v`'s real value, only read which type it was. But the question a program
actually needs to ask is usually not "what is this value's type" but "does this value satisfy
this type," and if it does, "give me the real value." This was already seen once on the way
out of the empty interface: `v.(Identifier)` recovered the value but without checking the
answer. The next lesson measures these two questions separately: a type assertion's two-value
form and the branch a type switch selects — two separate ways of asking the value.
