Dev Skills

Type assertions and type switches

Getting the concrete value back out of an interface, safely.

Topic 18 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Write a comma-ok type assertion that never panics.
  • Write a type switch that handles several concrete types and a default.
  • Fix a type switch where a broad interface case hides a more specific one.

The problem

An interface value hides what is inside it. A variable of type any might hold an int, a string, or your own struct. Most of the time that is the point: you call the methods and do not care.

Sometimes you do care. A type assertion asks "is the value inside this interface of type T?", and if so gives it back to you as a T.

Type assertions, and the comma-ok form

assert.go
package main
import "fmt"
func main() {
var v any = "hello"
s := v.(string) // "I am sure it is a string"
fmt.Println(s, len(s))
n, ok := v.(int) // "is it an int?" — no panic, just ok=false
fmt.Println(n, ok) // 0 false
if s, ok := v.(string); ok {
fmt.Println("it was a string:", s)
}
// v.(int) without the ok would panic:
// interface conversion: interface {} is string, not int
}
Use the one-value form only when a wrong type is a bug that should crash. Everywhere else, use comma-ok.

Type switches

When there are several types to check, a type switch does all the assertions in one go. Inside each case, the variable already has that case’s type.

switch.go
package main
import "fmt"
type Point struct{ X, Y int }
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nothing"
case int:
return fmt.Sprintf("an int, doubled is %d", x*2) // x is an int here
case string:
return fmt.Sprintf("a string of %d bytes", len(x)) // x is a string here
case Point:
return fmt.Sprintf("a point at %d,%d", x.X, x.Y)
case error:
return "an error: " + x.Error() // cases can be interfaces too
default:
return fmt.Sprintf("something else: %T", x)
}
}
func main() {
fmt.Println(describe(nil))
fmt.Println(describe(21))
fmt.Println(describe("héllo"))
fmt.Println(describe(Point{1, 2}))
fmt.Println(describe(fmt.Errorf("boom")))
fmt.Println(describe(3.5))
}
`.(type)` only works inside a `switch`. Cases are checked top to bottom, so put specific types before broad interfaces.

Errors are often wrapped inside other errors, which a plain type assertion cannot see through. The tool for that is covered in Error values in depth.

Watch out: A long type switch over your own types is often a sign that a method is missing. If every case calls a different "area" formula, give the types an Area() method and an interface instead. Switch on types at the edges, where values arrive as any (JSON, config, errors).
The short version
  • v.(T) gets a T out of an interface, and panics if it is not one.
  • t, ok := v.(T) never panics. ok tells you whether it worked.
  • switch x := v.(type) checks many types at once; in each case x has that type.
  • Cases run top to bottom, so put specific types before broad interfaces.
Remember: Comma-ok unless a wrong type is a bug.
Build this before moving on
Write func sum(values []any) (float64, error) that adds up ints, float64s and numeric strings (use strconv.ParseFloat), and returns an error naming the first value of any other type.
Coding challenge · checked by tests
Write Describe(v any) string with a type switch. An int gives "int N", a string gives "string of N bytes", a bool gives "bool", and anything else gives "unknown". Examples: 21 → "int 21", "héllo" → "string of 6 bytes", true → "bool", 3.5 → "unknown".

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Assert any(42).(string) without comma-ok and read the panic.
  2. 2In a type switch, put case error: above case *MyError: and see what happens.
  3. 3Write a case that lists two types, case int, int64:, and print %T of the variable inside it.
Check yourselfTake the Core language test