30. `any`, type assertions and type switches
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,okreports whether it worked, andnis 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.