Type Assertions and Type Switches

An interface value hides its concrete type. Sometimes you need it back: to use a method the interface doesn't list, to handle a few types differently, or to make sense of an any. A type assertion extracts one specific type; a type switch branches on several. This lesson covers both, plus checking for optional behaviour with interface assertions.

Type assertions

x.(T) asserts that the interface value x holds a T and returns it. Use the two-value "comma ok" form to test safely; the single-value form panics if the type is wrong:

GO
package main

import "fmt"

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

	s, ok := v.(string)
	fmt.Printf("%q %v\n", s, ok)

	n, ok := v.(int)
	fmt.Println(n, ok) // zero value and false: no panic

	fmt.Println(v.(string) + "!") // fine: it is a string
	fmt.Println(v.(int))          // panics
}
Output
"hello" true
0 false
hello!
panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
…
exit status 2

Use the single-value form only when a wrong type would be a bug in your own code. For anything that depends on input, use comma-ok.

Type switches

A type switch tests an interface value against several types in turn. In each case, the variable declared in the switch has that case's type:

GO
package main

import (
	"fmt"
	"strings"
)

func describe(v any) string {
	switch x := v.(type) {
	case nil:
		return "nothing"
	case int:
		return fmt.Sprintf("int, doubled: %d", x*2)
	case string:
		return fmt.Sprintf("string of length %d: %s", len(x), strings.ToUpper(x))
	case bool:
		if x {
			return "yes"
		}
		return "no"
	case []int:
		return fmt.Sprintf("slice with %d ints", len(x))
	case float32, float64:
		return fmt.Sprintf("a float: %v", x) // several types: x stays an any
	default:
		return fmt.Sprintf("something else (%T)", x)
	}
}

func main() {
	for _, v := range []any{42, "go", true, []int{1, 2}, 2.5, nil, map[string]int{}} {
		fmt.Println(describe(v))
	}
}
Output
int, doubled: 84
string of length 2: GO
yes
slice with 2 ints
a float: 2.5
nothing
something else (map[string]int)

When a case lists several types, the variable keeps the interface type, because Go can't know which of them it is.

Checking for optional behaviour

You can also assert to an interface type to ask "does this value have this method?" Standard library code does this to use a faster path when it's available. Here a report prints any item, and adds a summary for items that can provide one:

GO
package main

import "fmt"

type Summarizer interface {
	Summary() string
}

type Invoice struct {
	ID    int
	Total float64
}

func (i Invoice) Summary() string { return fmt.Sprintf("invoice #%d, %.2f due", i.ID, i.Total) }

type Note string

func report(items []any) {
	for _, it := range items {
		if s, ok := it.(Summarizer); ok {
			fmt.Println("-", s.Summary())
		} else {
			fmt.Println("-", it)
		}
	}
}

func main() {
	report([]any{Invoice{17, 99.5}, Note("call the bank"), 42})
}
Output
- invoice #17, 99.50 due
- call the bank
- 42

Errors and type switches

Type switches are useful on errors too, though for wrapped errors errors.As and errors.AsType (lesson 45) are usually better, because they also look inside wrapped errors:

GO
package main

import (
	"fmt"
	"os"
	"strconv"
)

func explain(err error) string {
	switch e := err.(type) {
	case nil:
		return "ok"
	case *strconv.NumError:
		return "bad number: " + e.Num
	case *os.PathError:
		return "file problem with " + e.Path
	default:
		return "other error: " + e.Error()
	}
}

func main() {
	_, err1 := strconv.Atoi("12x")
	_, err2 := os.Open("nope.txt")
	fmt.Println(explain(err1))
	fmt.Println(explain(err2))
	fmt.Println(explain(nil))
}
Output
bad number: 12x
file problem with nope.txt
ok

When to use them

Type switches are the right tool when the set of types is genuinely open — values decoded from JSON, an any from a library — or when checking for optional methods. If you find yourself switching on your own types to decide what to do, consider adding a method to an interface instead, so each type carries its own behaviour. And if you're using any just to accept several types of one kind, generics (lesson 47) give you type safety back.

Try it yourself

JSON decoded into any produces map[string]any, []any, string, float64, bool and nil. Write a function that counts how many numbers appear anywhere inside such a value.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
3
Next: interfaces from the standard library that your types should know about.