Skip to content
gohacks.dev
Basics

The Empty Interface and the any Alias in Go Explained

any is the empty interface and holds any value, at the cost of type safety. See when it fits, when generics are better and why []string is not []any.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go has a type that fits every value, and using it feels freeing until the compiler stops helping you. The empty interface, now written any, has no methods, so every type satisfies it. That makes it the universal parameter for fmt.Println, the destination for decoded JSON and the escape hatch for code that cannot say what it holds. It also turns a typo in a type name from a compile error into a panic during a request. This article explains what any is, where it earns its place, and where a type parameter or a real interface is the better design. It builds on interfaces and type assertions, and sits in the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

One type, two spellings

Before Go 1.18 you wrote interface{}. The release added any as a predeclared alias, defined as type any = interface{}. The two are identical and interchangeable everywhere:

main.goGo
package main

import "fmt"

func main() {
	var a any = 42
	var b interface{} = a

	fmt.Println(a == b)
	fmt.Printf("%T %v\n", a, a)

	a = "now a string"
	fmt.Printf("%T %v\n", a, a)

	values := []any{1, "two", 3.0, []int{4}, map[string]int{"five": 5}, nil}
	for _, v := range values {
		fmt.Printf("%T ", v)
	}
	fmt.Println()
}
Text
true
int 42
string now a string
int string float64 []int map[string]int <nil>

The variable changes type at run time, which is the entire appeal and the entire risk. %T prints what is stored. The list holds six unrelated values, and only the %T and %v verbs know what to do with them without an assertion. any is the spelling I use in new code, because it is shorter and says what it means, and gofmt -r 'interface{} -> any' -w . converts old code mechanically.

What you can do with an any

Very little, on its own. An any has no methods and no operators. You cannot add two of them, index one or call a function on it. Before using the value, you have to recover the type:

Go
var v any = 42
n := v.(int)        // assertion, panics if wrong
s, ok := v.(string) // comma ok form

This is the cost the compiler no longer checks. If you assert string where the value is int, you get the panic from the type assertion article at run time. A function that accepts any is promising to handle every possible input, and most do not.

Where any belongs

The legitimate uses have something in common: the code genuinely does not know the type and does not need to.

  • Formatting and logging. fmt.Println(args ...any) and slog attributes take values of any type and render them. They look at the type through reflection and need no help from you.
  • Decoding unknown data. A JSON object whose shape varies decodes into map[string]any, and a type switch handles the pieces, as in the decoded document walker.
  • Heterogeneous containers. A template engine's data, a context value, a configuration tree. The value genuinely varies by key.
  • Reflection-based libraries. ORMs, validators and serializers take any and inspect it with reflect.

Outside those, if a function's body begins with a type switch, ask whether the callers could pass an interface with the method the function actually needs.

A slice of strings is not a slice of any

This is the trap that sends people to the forums. An any is a two-word value, and a string is a two-word value with a different layout, so a []string and a []any are different types with different memory shapes. Go will not convert one into the other:

main.goGo
package main

import "fmt"

func printAll(items ...any) {
	for _, it := range items {
		fmt.Println(it)
	}
}

func main() {
	names := []string{"ada", "grace"}
	printAll(names...)
}
Text
# example.test/lab
./main.go:13:11: cannot use names (variable of type []string) as []any value in argument to printAll

The error says you cannot use names as []any in the argument. The ... spread only works when the slice already has the parameter's element type. The conversion is explicit and costs a loop and an allocation:

main.goGo
package main

import "fmt"

func printAll(items ...any) {
	for _, it := range items {
		fmt.Println(it)
	}
}

func main() {
	names := []string{"ada", "grace"}

	args := make([]any, len(names))
	for i, n := range names {
		args[i] = n
	}
	printAll(args...)
}
Text
ada
grace

If you find yourself writing this loop often, the function should probably be generic: a func PrintAll[T any](items []T) accepts any slice type and does the work with no conversion. That is the point at which any stops being the right tool.

Equality can panic

Two interface values are equal when they hold the same dynamic type and equal values. If the type is not comparable, a slice, a map or a function, comparing at run time panics, and the compiler cannot warn you because the dynamic type is unknown until the program runs:

main.goGo
package main

import "fmt"

func main() {
	var a, b any = []int{1}, []int{1}

	fmt.Println(a == b)
}
Text
panic: runtime error: comparing uncomparable type []int

goroutine 1 [running]:
main.main()
...

The message is panic: runtime error: comparing uncomparable type []int. This matters for any used as a map key too: inserting a slice as a key panics with the hashing variant of the same message, which the maps guide demonstrates. If you need to compare arbitrary values, reflect.DeepEqual handles them and carries its own costs and surprises, such as treating a nil slice and an empty slice as different. In tests, the standard library's slices.Equal and maps.Equal are better when you know the types.

The cost of boxing

Putting a value into an any can allocate. Small integers, below 256, and pointers box without allocating. A larger integer, a string and a multi-field struct each allocated once in my measurements, which are in the interfaces guide. That is why a hot path that accepts any shows up in allocation profiles, and why replacing it with a generic function or a concrete type reduces allocs/op. The cost is invisible in normal code and meaningful in inner loops.

When generics are the better design

Take a function that reports whether a slice contains a value. With any it has to compare through the interface and accept a slice of anything, so the caller converts. With a type parameter it keeps the types:

main.goGo
package main

import "fmt"

func ContainsAny(items []any, target any) bool {
	for _, it := range items {
		if it == target {
			return true
		}
	}
	return false
}

func Contains[T comparable](items []T, target T) bool {
	for _, it := range items {
		if it == target {
			return true
		}
	}
	return false
}

func main() {
	names := []string{"ada", "grace"}

	fmt.Println(Contains(names, "ada"), Contains(names, "linus"))
	fmt.Println(Contains([]int{1, 2, 3}, 2))

	boxed := []any{"ada", 7}
	fmt.Println(ContainsAny(boxed, "ada"), ContainsAny(boxed, 8))
}
Text
true false
true
true false

The generic version rejects Contains(names, 5) at compile time, because 5 is not a string. The any version accepts it and returns false, silently. That is the trade: any is flexible and unchecked, and a type parameter is flexible and checked. Since Go 1.21 the standard library has slices.Contains for exactly this case, so you would not write it yourself. The rule I follow is to reach for any when the type truly varies at run time, to use an interface when the behavior varies, and to use a type parameter when the same algorithm applies to many types.

Where it stays a bad idea

APIs that take any and document "must be a string or an int" are using the type system as a comment. A function that returns any, such as func Get(key string) any, pushes an assertion into every caller and an unrecoverable mistake into every typo. Configuration objects built from map[string]any lose field names, types and autocomplete, so a struct with named fields is better as soon as the keys are known. When you must accept any value, write down what you do with it, and test the unexpected types, because the compiler will not. The specification on the empty interface is brief, and the Go 1.18 release notes describe the alias along with generics.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.