Type Assertions and Type Switches

Get the concrete type back out of an interface value with the comma-ok form, or handle several types at once with a type switch.

An interface value hides the concrete type inside it. Sometimes you need that type back: to reach a field, or to behave differently for one kind of value. A type assertion does this. It is written x.(T), where x is an interface value and T is the type you think is in there.

Use the comma-ok form

There are two forms. v := x.(T) returns the value and stops the program with a panic if the guess was wrong. A panic ends the program unless it is recovered, which is covered later. v, ok := x.(T) never panics: on a wrong guess, ok is false and v is the zero value of T. Use the second form unless a wrong type genuinely means your program is broken.

T may also be another interface. Then the assertion asks whether the stored type has that interface's methods too, which is how code tests for an optional extra ability.

Longhand and shorthand

A chain of comma-ok assertions is the longhand. A type switch is the shorthand for exactly that chain, and the two forms below behave identically.

// Longhand
if n, ok := v.(int); ok {
	return n * 2
} else if s, ok := v.(string); ok {
	return len(s)
}
return 0

// Shorthand
switch t := v.(type) {
case int:
	return t * 2
case string:
	return len(t)
}
return 0

Inside each single-type case, the variable has that concrete type. In a case listing two types, it keeps the interface type instead.

Example

Example · go
package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Cat struct{ Name string }

func (c Cat) Speak() string { return "meow" }

type Dog struct{ Name string }

func (d Dog) Speak() string { return "woof" }

// describe uses a type switch to recover the concrete type.
func describe(s Speaker) string {
	switch v := s.(type) {
	case Cat:
		return "cat named " + v.Name
	case Dog:
		return "dog named " + v.Name
	default:
		return "some other animal"
	}
}

func main() {
	var s Speaker = Cat{Name: "Mo"}

	// Comma-ok form: ok reports whether the guess was right.
	c, ok := s.(Cat)
	fmt.Println(c.Name, ok)

	// A wrong guess does not panic. d is the zero Dog.
	d, ok := s.(Dog)
	fmt.Printf("%q %v\n", d.Name, ok)

	fmt.Println(describe(s))
}

// Output:
// Mo true
// "" false
// cat named Mo

When to use it

  • Reach a field on the concrete struct after receiving it through an interface.
  • Handle decoded JSON values whose type is only known at run time.
  • Check whether a writer also supports flushing before calling Flush on it.

More examples

A type switch over any value

A type switch handles a nil interface with case nil, groups types in one case, and catches everything else in default. Use it when values arrive with no fixed type, such as data decoded from JSON or arguments passed as any.

Example · go
package main

import "fmt"

// label accepts any value. any lists no methods, so everything
// satisfies it.
func label(v any) string {
	switch t := v.(type) {
	case nil:
		return "nothing"
	case int:
		return fmt.Sprintf("int %d", t*2)
	case string:
		return fmt.Sprintf("string of %d bytes", len(t))
	case []int:
		return fmt.Sprintf("slice of %d ints", len(t))
	case float32, float64:
		// Two types in one case, so t keeps the type any and
		// cannot be used in arithmetic here.
		return fmt.Sprintf("a float: %v", t)
	default:
		// %T prints the concrete type that was stored.
		return fmt.Sprintf("unhandled %T", v)
	}
}

func main() {
	fmt.Println(label(21))
	fmt.Println(label("hello"))
	fmt.Println(label([]int{1, 2, 3}))
	fmt.Println(label(1.5))
	fmt.Println(label(nil))
	fmt.Println(label(true))
}

// Output:
// int 42
// string of 5 bytes
// slice of 3 ints
// a float: 1.5
// nothing
// unhandled bool

Asking whether a value has an extra ability

Asserting to a second interface tests for optional behaviour without widening the interface everyone must satisfy. The standard library uses this pattern constantly, for example to flush an HTTP response only when the underlying writer supports it.

Example · go
package main

import "fmt"

type Store interface {
	Put(key, value string)
}

// Flusher is optional. Not every Store has it.
type Flusher interface {
	Flush() int
}

type buffered struct {
	data map[string]string
}

func (b *buffered) Put(key, value string) { b.data[key] = value }

// Flush reports how many entries it wrote out.
func (b *buffered) Flush() int {
	n := len(b.data)
	b.data = map[string]string{}
	return n
}

type plain struct{}

func (plain) Put(key, value string) {}

// save asks at run time whether this Store can also flush.
func save(s Store) {
	s.Put("a", "1")
	s.Put("b", "2")
	if f, ok := s.(Flusher); ok {
		fmt.Println("flushed", f.Flush())
		return
	}
	fmt.Println("no flush support")
}

func main() {
	save(&buffered{data: map[string]string{}})
	save(plain{})
}

// Output:
// flushed 2
// no flush support

The same logic, both ways

Reading these side by side makes the type switch recognisable as shorthand rather than new syntax. Keep the longhand when only one type matters, and switch to the type switch as soon as a third case appears.

Example · go
package main

import "fmt"

// Longhand: one comma-ok assertion per type.
func kindLong(v any) string {
	if n, ok := v.(int); ok {
		return fmt.Sprintf("int %d", n)
	}
	if s, ok := v.(string); ok {
		return "string " + s
	}
	return "other"
}

// Shorthand: the identical chain written as a type switch.
func kindShort(v any) string {
	switch t := v.(type) {
	case int:
		return fmt.Sprintf("int %d", t)
	case string:
		return "string " + t
	default:
		return "other"
	}
}

func main() {
	for _, v := range []any{7, "hi", 1.5} {
		fmt.Println(kindLong(v), "|", kindShort(v))
	}
}

// Output:
// int 7 | int 7
// string hi | string hi
// other | other

Discussion

  • Be the first to comment on this lesson.