30. `any`, type assertions and type switches

📖 Reading · 11 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

An interface with no methods is satisfied by every type — there's nothing to satisfy. Go calls it any (and, before Go 1.18, interface{}; they are the same type, and you'll see both). This lesson is about putting values into one and getting them back out safely.

any holds anything

package main

import "fmt"

func main() {
    var v any

    v = 42
    fmt.Printf("%v %T\n", v, v)

    v = "hello"
    fmt.Printf("%v %T\n", v, v)

    v = []int{1, 2, 3}
    fmt.Printf("%v %T\n", v, v)

    things := []any{1, "two", 3.0, true, nil}
    fmt.Println(things...)
}

This is how fmt.Println(a ...any) accepts anything you throw at it.

But notice what you can't do: v + 1 won't compile even when v holds an int. The static type is any, which has no methods and no operators. To use the value you must first recover its real type.

Type assertions

package main

import "fmt"

func main() {
    var v any = "hello"

    s := v.(string)
    fmt.Println(s, len(s))

    n, ok := v.(int)
    fmt.Println(n, ok)
}

v.(string) asserts "the concrete type in here is string, give it to me".

There are two forms, and the difference is critical:

  • s := v.(string)panics if the assertion is wrong.
  • n, ok := v.(int) — the comma-ok form: never panics, ok reports whether it worked, and n is the zero value when it didn't.

Use the single-value form only when a wrong type is a bug you want to crash on. Everywhere else, use comma-ok — the same shape you already know from map lookups.

package main

import "fmt"

func describe(v any) {
    if s, ok := v.(string); ok {
        fmt.Println("a string of length", len(s))
        return
    }
    if n, ok := v.(int); ok {
        fmt.Println("an int, doubled:", n*2)
        return
    }
    fmt.Println("something else:", v)
}

func main() {
    describe("hello")
    describe(21)
    describe(3.14)
}

Type switches

Chained assertions get tedious. The type switch does the same job with a shape you'll recognise from the control-flow module:

package main

import "fmt"

func describe(v any) {
    switch x := v.(type) {
    case nil:
        fmt.Println("nothing at all")
    case int:
        fmt.Println("int, doubled:", x*2)
    case string:
        fmt.Println("string of length", len(x))
    case bool:
        fmt.Println("bool, negated:", !x)
    case []int:
        fmt.Println("int slice with", len(x), "elements")
    default:
        fmt.Printf("unhandled type %T\n", x)
    }
}

func main() {
    for _, v := range []any{1, "hi", true, []int{1, 2}, 3.14, nil} {
        describe(v)
    }
}

switch x := v.(type) is special syntax — .(type) is only legal here. In each case, x already has that case's type, so x*2 and len(x) compile.

Two cases in one branch (case int, int64:) is allowed, but then x stays any, because Go can't pick one type for it.

Asserting to an interface

Assertions aren't limited to concrete types. You can ask "does this also implement that?":

type Stringer interface {
    String() string
}

type Point struct{ X, Y int }

func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }

func show(v any) {
    if s, ok := v.(Stringer); ok {
        fmt.Println("stringer:", s.String())   // -> stringer: (1,2)
        return
    }
    fmt.Println("plain:", v)                   // -> plain: 42
}

func main() {
    show(Point{1, 2})
    show(42)
}

(That box is reference-only: asserting from any to an interface you defined yourself is one of the few things the in-browser interpreter can't do. Asserting to a concrete type works fine, as every other box here shows.)

The standard library uses this constantly: fmt checks whether your value is a Stringer, encoding/json checks for a Marshaler, and errors are unwrapped by checking for an Unwrap() error method. It's how a package offers an optional capability without demanding it.

When not to use any

any throws away every guarantee the compiler gives you. Before Go had generics it was the only way to write a container that held "anything", and the result was casts everywhere and runtime panics.

Since Go 1.18 there's a better tool for that job:

// the old way — compiles, but every use needs an assertion
func First(items []any) any { return items[0] }

// the modern way — see the Generics module
func First[T any](items []T) T { return items[0] }

Reach for any when the value's type genuinely isn't known until runtime — decoding arbitrary JSON, a printf-style API, a plugin boundary. For "this function works with several types", use generics.

Working with decoded JSON

The place you'll meet any most often in real code:

package main

import (
    "encoding/json"
    "fmt"
    "sort"
)

func main() {
    data := []byte(`{"name":"Ada","age":36,"tags":["go","math"],"active":true}`)

    var parsed map[string]any
    if err := json.Unmarshal(data, &parsed); err != nil {
        fmt.Println("error:", err)
        return
    }

    keys := make([]string, 0, len(parsed))
    for k := range parsed {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    for _, k := range keys {
        fmt.Printf("%-8s %-10T %v\n", k, parsed[k], parsed[k])
    }

    if name, ok := parsed["name"].(string); ok {
        fmt.Println("name is", name)
    }
    if age, ok := parsed["age"].(float64); ok {
        fmt.Println("age next year:", int(age)+1)
    }
}

Look at the type of age: float64, not int. JSON has one number type, so decoding into any gives you float64 for every number, always. Asserting .(int) there silently fails — a classic Go bug.

(The stdlib module shows the better approach: decode into a struct and let the compiler give you real types.)

Your turn

Write sumNumbers(values []any) int that adds up only the int values and ignores everything else:

60
package main

import "fmt"

// write sumNumbers here

func main() {
    values := []any{10, "twenty", 20, true, 30, 3.5}
    fmt.Println(sumNumbers(values))
}
package main

import "fmt"

func sumNumbers(values []any) int {
    total := 0
    for _, v := range values {
        if n, ok := v.(int); ok {
            total += n
        }
    }
    return total
}

func main() {
    values := []any{10, "twenty", 20, true, 30, 3.5}
    fmt.Println(sumNumbers(values))
}

Next: building bigger interfaces out of small ones.