12.4 The Empty Interface any (and interface{}): The Universal Type
Alright, let’s talk about the any type, or as it was known in its more verbose youth, interface{}. This is Go’s universal type, the linguistic equivalent of a cardboard box you use when you move house. You can shove absolutely anything in there—your fine china, your collection of novelty mugs, that weird statue your aunt gave you—but once it’s in the box, you lose all information about what it is. You just know it’s something.
Its real name is the empty interface: an interface with zero methods. And since every type in Go satisfies at least zero methods, every single type automatically satisfies the empty interface. It’s the ultimate loophole. It’s what you reach for when you need a function to accept literally anything, or when you’re dealing with data whose structure you don’t know at compile time (like JSON unmarshaling). It’s incredibly powerful and, if used carelessly, a fantastic way to reintroduce all the type-related bugs you thought you’d escaped by using a statically typed language.
Why any Exists (And Why We Use It)
You use any when you need to write code that is agnostic to the type it’s operating on. The most common, and arguably most justified, use case is when dealing with data from the outside world. Think about the json.Unmarshal function. How could its authors possibly write a function that handles your specific struct for a user, a product, a configuration file, and every other conceivable JSON structure? They can’t. So, they don’t try. They say, “Give us a blob of bytes and we’ll put the parsed data somewhere. You tell us where by passing us a any (well, an interface{}).”
package main
import (
"encoding/json"
"fmt"
)
// You know this is a user, but the json package doesn't.
var jsonBlob = []byte(`{"name": "Alice", "age": 30}`)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
var userData any // Could also be written as var userData interface{}
err := json.Unmarshal(jsonBlob, &userData)
if err != nil {
panic(err)
}
// Let's see what we got...
fmt.Printf("Unmarshaled data: %#v\n", userData)
// Prints: Unmarshaled data: map[string]interface {}{"age":30, "name":"Alice"}
}
See? The JSON package didn’t know about our User type, so it did the only sensible thing: it unmarshaled the data into a map[string]any. The keys are strings (of course), but the values could be numbers, strings, booleans, more maps, or slices—so they have to be any.
The Inevitable Check: Type Assertions and Type Switches
Putting something in an any box is easy. The real work, and the source of most bugs, is getting it back out safely. You can’t call .Name on an any. The compiler will quite rightly stop you because it has no guarantee what’s inside. To access the concrete value, you must perform a type assertion.
A type assertion is basically you telling the compiler, “Trust me, I know what’s in here. It’s a string.” You say this with the syntax value := myAny.(ConcreteType).
// Attempting a type assertion
theMap := userData.(map[string]any) // This works for our example
name := theMap["name"].(string) // Assert that the value is a string
age := theMap["age"].(int) // Assert that the value is an int
fmt.Printf("User %s is %d years old.\n", name, age)
But what if you’re wrong? What if the “age” field was actually the string “thirty”? Then theMap["age"].(int) would cause a panic. Your program would blow up. This is the primary pitfall of using any: you shift the burden of type safety from the compiler (at compile time) to you (at runtime).
To handle this gracefully, use the “comma, ok” idiom to perform a safe type assertion.
// Safe type assertion
if nameVal, ok := theMap["name"].(string); ok {
fmt.Println("Name is:", nameVal)
} else {
fmt.Println("'name' is not a string or doesn't exist!")
}
// This won't panic, even if it fails.
if ageVal, ok := theMap["age"].(int); ok {
fmt.Println("Age is:", ageVal)
} else {
fmt.Println("'age' is not an int!")
}
For more complex scenarios with multiple possible types, a type switch is your best friend. It’s far cleaner than a chain of if/else statements.
func inspectValue(v any) {
switch val := v.(type) {
case string:
fmt.Printf("It's a string: %s\n", val)
case int:
fmt.Printf("It's an int: %d\n", val)
case float64:
fmt.Printf("It's a float64: %f\n", val) // Common from JSON numbers!
case map[string]any:
fmt.Println("It's another messy map...")
default:
fmt.Printf("It's something else I don't care about: %T\n", val)
}
}
Notice the special v.(type) syntax that only works inside a switch block.
any vs. interface{}: A Matter of Taste
You’ll see both. interface{} is the original spelling, baked into the language since day one. any is a type alias that was introduced in Go 1.18 as a gift to our collective sanity. It means exactly the same thing. It’s literally defined in the builtin package as type any = interface{}.
Use whichever you prefer. any is clearer and more intention-revealing. It reads like English: “This function accepts any type.” interface{} reads more like “implementation detail.” I strongly recommend any for all new code. The only reason to use interface{} now is if you’re maintaining ancient code and want to be consistent.
Best Practices and When to Avoid It
The empty interface is a sharp tool. Respect it.
- Avoid it in your own APIs whenever possible. If you’re writing a function that should only work with a
stringand anint, define it asfunc Foo(s string, i int). Don’t lazily make itfunc Foo(a, b any)and force your caller to do the type checking you were too lazy to do. You’ve just made your function more dangerous and harder to use. - It’s a last resort, not a first choice. Its main legitimate uses are for handling unknown data (like in the
jsonpackage) or in truly generic data structures (before generics existed, this is how you’d write a container for “any” type). Now that generics are here, you should use them instead for most container-like use cases. - You are the type system now. When you use
any, you are opting out of Go’s compile-time type safety. The onus is entirely on you to check types correctly at runtime with assertions or switches. Write tests for those code paths. A missed check is a runtime panic waiting to happen.
So, use any when you genuinely need that level of flexibility, but always be aware that you’re trading the compiler’s help for a whole lot of potential runtime trouble. It’s the Go programmer’s necessary evil.