What Is the Difference Between interface{} and any in Go

`any` is simply a predeclared alias for `interface{}` introduced in Go 1.18; they are functionally identical at runtime but `any` improves code readability by reducing visual noise.

When the label changes but the box stays the same

You open a fresh Go repository and see func Process(v any). You switch to a three-year-old library and see func Process(v interface{}). Your brain trips. Are these two different types? Do you need a type conversion? Does the compiler care? The short answer is no. They are the exact same type. any is just a predeclared alias for interface{} added in Go 1.18. The compiler treats them identically. The difference lives entirely in the human reading the code.

What an empty interface actually is

Go interfaces are contracts. An interface with methods says this value must be able to do these things. An empty interface, written as interface{}, says this value can be anything. It has no methods, so every single type in the language satisfies it automatically. Think of it like a shipping crate with no lock. You can put a laptop, a brick, or a watermelon inside. The crate does not care what goes in.

When Go 1.18 arrived, the language team realized that interface{} looked like a syntax mistake to newcomers. It visually resembles a generic type parameter or a malformed definition. They added any as a predeclared identifier to make the intent obvious. It is the same crate. The label just changed. The compiler resolves any to interface{} during lexical analysis. No new type is created. No new runtime behavior is introduced. You are reading the same underlying construct with a cleaner name.

Minimal proof of interchangeability

Here is the simplest proof that they are interchangeable. The compiler allows direct assignment without casting or conversion.

package main

import "fmt"

func main() {
    // Empty interface holds an int. The runtime stores type and value.
    var legacy interface{} = 42
    // Alias holds a string. Identical underlying representation.
    var modern any = "hello"

    // Direct assignment works because the compiler sees the same type.
    legacy = modern
    modern = legacy

    // Type assertion extracts the concrete value back out.
    if s, ok := modern.(string); ok {
        fmt.Println("Recovered:", s)
    }
}

How the compiler and runtime see it

When you compile this, the type checker does not create two separate type entries. It resolves any to interface{} before semantic analysis even begins. At runtime, both variables use the exact same memory layout. Go represents empty interfaces as a two-word structure. The first word is a pointer to the type descriptor. The second word is a pointer to the actual data. The runtime does not know or care whether you typed any or interface{} in your source file. It only sees the type descriptor and the payload.

This design keeps the type system simple. There is no hidden performance penalty for using the newer alias. The binary output is byte-for-byte identical. The Go team deliberately chose an alias instead of a new type to avoid breaking existing code and to keep the runtime interface representation unified. You can pass any to a function expecting interface{} and vice versa without triggering any hidden conversions. The type system treats them as synonyms.

Realistic usage in modern codebases

Real codebases use the empty interface when the type truly cannot be known ahead of time. JSON unmarshaling is the classic case.

package main

import (
    "encoding/json"
    "fmt"
)

// UnmarshalJSON demonstrates why we need a catch-all type.
func UnmarshalJSON(data []byte) {
    // The target must be a pointer to hold the decoded result.
    var result any
    // json.Unmarshal inspects the JSON structure at runtime.
    // It populates result with a map, slice, or primitive.
    err := json.Unmarshal(data, &result)
    if err != nil {
        fmt.Println("decode failed:", err)
        return
    }
    // The concrete type depends entirely on the input payload.
    fmt.Printf("Decoded type: %T\n", result)
}

In this scenario, any signals to the reader that the function accepts arbitrary data. Using interface{} here would work perfectly, but any removes the visual friction. The Go community convention is straightforward. Write any in new code. Leave interface{} alone in legacy code. You do not need to run a mass refactor. The compiler does not care, and neither should your CI pipeline. If you are already touching a file, swap it. If you are not, leave it. The goal is readability, not ritual.

Pitfalls and runtime traps

The empty interface is powerful, but it strips away compile-time safety. When you pass any, you are telling the compiler to stop checking types. The burden shifts to runtime. If you forget to verify the type before extraction, your program panics. The runtime throws panic: interface conversion: interface is int, not string if you force a wrong type assertion. Always use the comma-ok idiom to check first.

Another common trap is confusing any with generics. Generics let you write type-safe code that works with multiple types. any throws type safety away entirely. If you can express your function with a type parameter like func Process[T comparable](v T), do it. Reserve any for situations where the type genuinely varies at runtime. The compiler will reject cannot use type parameter as interface{} if you try to mix them incorrectly, but the real issue is architectural. You are usually reaching for the wrong tool.

Type switches are the standard way to handle multiple known types inside an any variable. They are safer than chained type assertions because the compiler verifies that you covered the cases you intended. If you drop a case and the runtime encounters an unexpected type, the switch simply skips it. No panic. No crash. Just silent failure, which is why you should always include a default case that logs or returns an error. The worst bug with any is the one that silently ignores invalid input.

Community conventions to follow

Go naming conventions apply here too. Public functions start with a capital letter. Private ones start lowercase. When you accept any, you are accepting a public contract that says I handle everything. That is a heavy promise. Keep those functions small. Document what types you actually expect to receive. The community mantra accept interfaces, return structs still holds. If you can narrow the interface to something like io.Reader or fmt.Stringer, do it. any should be the exception, not the default.

The underscore convention also matters when working with dynamic data. If you call a function that returns a value and an error, and you deliberately ignore the error, you write result, _ := Decode(data). This tells the reader you considered the error and chose to drop it. Do not use _ with any type assertions unless you are certain the type will always match. Silent failures in dynamic code are harder to debug than compile-time errors.

When to reach for which tool

Use any when you are writing new code and need a catch-all type for dynamic data like JSON payloads or plugin systems. Use interface{} when maintaining legacy codebases where changing the signature would trigger unnecessary diffs across hundreds of files. Use a concrete type or a narrow interface when the function only needs to work with a specific set of methods. Use generics when you want to preserve compile-time type checking while supporting multiple types. Use type switches when you need to handle several known concrete types inside a single function.

any is just a label. The type system is what matters.

Where to go next