Skip to content
Theory · Interfaces · lesson 7 of 10

Type switches

In the previous lesson a type assertion asked a single question, "Are you a driver?", and that was enough. But if a newcomer may turn out to be a driver, a cook or an accountant, the questions pile up. Picture an HR desk at the entrance: they see who has come and send each person to their department—the driver to the garage, the cook to the kitchen.

Without such a desk, assertions with ok line up into a chain. Each new type adds another else if branch with its own check:

GO
package main

import "fmt"

func main() {
    var v any = "Go"
    if n, ok := v.(int); ok {
        fmt.Println("int", n)
    } else if s, ok := v.(string); ok {
        fmt.Println("string", len(s)) // string 2
    }
}

Let's go through the checks in order:

  1. The first asks whether v holds an int and gets false.
  2. The second finds a string and prints string 2.

With two types the chain is still readable, but with five it stretches across the screen. A type switch writes the same check more compactly, and each new type adds one case line to it.

The v.(type) form

A type switch compares the dynamic type of a value with the types in its case branches. This is the HR desk at the entrance. The header switch x := v.(type) has these parts:

  • switch—the keyword, as in an ordinary switch;
  • x :=—a variable that gets the value from v in each branch;
  • v.(type)—where the assertion v.(int) would be, the keyword type stands.

The branches are checked from top to bottom, and the first match runs, as in an ordinary switch. The function size returns the number itself for an int and the length for a string or a slice:

GO
func size(v any) int {
    switch x := v.(type) {
    case int:
        return x
    case string:
        return len(x)
    case []int:
        return len(x)
    case nil:
        return 0
    default:
        return -1
    }
}

Two rules of the form:

  • if you do not need the values, you can skip the variable: switch v.(type);
  • the form v.(type) is allowed only in a switch header; anywhere else the program does not compile.

The variable in each branch

Which type x gets depends on the kind of branch. Let's go through the three kinds in the function size.

A single-type branch

In such a branch, the variable x gets the branch's type. In case int, x has type int, so it can be returned as a number. In case string, x has type string, so len applies to it. Whoever reaches the garage is certainly a driver, and they are treated as one.

The case nil branch

This branch runs when nothing was put into v. Here x keeps the type any.

Remember the pair from the previous lesson: a nil pointer does not land in case nil. For var p *int, the call size(p) returns -1, because the dynamic type *int is already in the interface value.

The default branch

The default branch runs for every other type not listed in a case. Here x also keeps the type any, since any type could have landed there.

Trying it on values

Let's try the function on different values:

GO
fmt.Println(size(7), size("Go"), size([]int{1, 2, 3})) // 7 2 3
fmt.Println(size(nil), size(2.5))                      // 0 -1

Let's go through each call:

  1. size(7) returns the number as is: 7.
  2. size("Go") takes the string's length: 2.
  3. size([]int{1, 2, 3}) takes the length of a three-element slice: 3.
  4. size(nil) lands in case nil and gives 0.
  5. 2.5 has type float64, and there is no such branch, so default runs and returns -1.

Common mistake

One branch listed two types and applied arithmetic to x:

GO
var v any = 21
switch x := v.(type) {
case int, float64:
    fmt.Println(x * 2) // ✗
}
TEXT
invalid operation: x * 2 (mismatched types any and untyped int)

When a case lists more than one type, the compiler does not know which of them x will hold. So in such a branch x stays of type any, and it cannot be multiplied. Here the HR-desk picture ends: a person behind a shared door can be asked again, but the compiler decides everything at build time. Split the types into separate branches, case int and case float64.

Key points

  • switch x := v.(type) picks a branch by the dynamic type of v; branches are checked from top to bottom.
  • In a single-type branch x has that type; in case nil, default and a multi-type branch it has the type any.
  • case nil runs only when the interface holds nothing; a nil pointer does not land there.
  • The form v.(type) is allowed only in a switch header.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way