Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Type Assertions and Type Switches

Recommended resources

Hello! Welcome back to your course on Go.

In our last lesson, we introduced the empty interface, any, as Go's tool for handling values of unknown types. We saw how to put any value into a variable of type any and, crucially, how to safely get it back out by checking for a single, specific type using a type assertion with the "comma ok" idiom.

This is great if you expect one type, but what if a value could be one of several? Chaining multiple if/else statements is clumsy and hard to read. Today, we'll learn a more elegant and robust way to handle this. Your learning outcome is to employ type assertions and type switches to work with any safely. We'll master the patterns for situations where an interface value could hold one of several possible concrete types—a common scenario when building DevOps tools.

1. The Challenge: Handling Multiple Possible Types

Let's start with a scenario. You're writing a tool that processes metrics from different sources. Some metrics are integers (int), some are floating-point numbers (float64), and some might be strings representing a status (string). You might store them in a slice of any:

func main() {
    metrics := []any{100, 25.5, "status:ok", 42, "status:error"}
    processMetrics(metrics)
}

How would you write processMetrics? Using what we learned last time, you could use a chain of if-else if statements with type assertions:

func processMetrics(metrics []any) {
    for _, metric := range metrics {
        if v, ok := metric.(int); ok {
            fmt.Printf("Processing integer metric: %d\n", v)
        } else if v, ok := metric.(float64); ok {
            fmt.Printf("Processing float metric: %.2f\n", v)
        } else if v, ok := metric.(string); ok {
            fmt.Printf("Processing string metric: '%s'\n", v)
        } else {
            fmt.Printf("Warning: Unknown metric type: %T\n", metric)
        }
    }
}

This code is safe—it won't panic—but it's verbose. As you add more types, the chain of if-else if blocks grows, making the code harder to read and maintain. Go provides a much cleaner solution for this exact problem: the type switch.


2. Type Assertions and Type Switches

Before we replace the code above, let's solidify the concepts of type assertions and type switches. A type assertion lets you look inside an interface value to discover its underlying concrete type. A type switch is a control structure that performs several type assertions in a clean, sequential way.

To see both in action, let's watch a short segment from a talk by Ricardo Gerardi. He demonstrates asserting a type and then introduces the type switch as a more structured alternative.

What Makes Golang Go: The Power of Go Interfaces — Ricardo Gerardi

This clip from 'What Makes Golang Go: The Power of Go Interfaces' clearly demonstrates type assertions and then introduces type switches as a natural evolution.

Watch the segment from 40:36 to 44:15. Pay attention to: The syntax for a safe type assertion using the 'comma ok' idiom. How a type switch provides a case for each potential type. Notice that within each case block, the variable has the correct concrete type.

As you saw in the video, the type switch provides a much more readable structure.


3. Rewriting Our Example with a Type Switch

Now, let's refactor our processMetrics function using a type switch.

The syntax switch v := i.(type) is unique to type switches. It iterates through a series of cases, checking if the interface i holds a value of the type specified in the case. If a match is found, the value is assigned to the variable v with the correct concrete type for use within that case block.

func processMetrics(metrics []any) {
    for _, metric := range metrics {
        switch v := metric.(type) {
        case int:
            fmt.Printf("Processing integer metric: %d\n", v)
        case float64:
            fmt.Printf("Processing float metric: %.2f\n", v)
        case string:
            fmt.Printf("Processing string metric: '%s'\n", v)
        default:
            fmt.Printf("Warning: Unknown metric type: %T\n", v)
        }
    }
}

This version is functionally identical to the if-else if chain but is significantly more idiomatic, readable, and extensible. Adding a new type is as simple as adding a new case.

For a detailed textual explanation with more examples, please read through the following guide. It breaks down both type assertions and type switches with clear code snippets.

Understanding Type Assertion and Type Switch in Go

The article 'Understanding Type Assertion and Type Switch in Go' on Leapcell provides excellent, self-contained examples for both concepts.

Please read the sections 'Type Assertion: Peeking Under the Hood' and 'Type Switch: Handling Multiple Types Gracefully'. Focus on: The two forms of type assertion: the panicking form and the safe 'comma-ok' idiom. The DescribeShape example, which is a great illustration of how a type switch works. The explanation that inside each case block, the variable v has the asserted type, allowing you to access its specific fields and methods.

Test your understanding!

Imagine you're parsing a simple YAML configuration file for a CI/CD pipeline step. The timeout can be specified as an integer (seconds) or a string (e.g., "5m"). The value is stored in a variable val of type any.

Write a function parseTimeout(val any) int that takes this any value and returns the timeout in seconds as an int.

  • If val is an int, return it directly.
  • If val is a string "5m", return 300.
  • If val is a string "1m", return 60.
  • For any other value or type, return a default timeout of 900 seconds.

Use a type switch to implement this logic.

Show answer
package main

import "fmt"

func parseTimeout(val any) int {
    switch v := val.(type) {
    case int:
        return v
    case string:
        if v == "5m" {
            return 300
        } else if v == "1m" {
            return 60
        }
    }
    // For any other string value or any other type, fall through
    // to the default return.
    return 900
}

func main() {
    timeout1 := parseTimeout(120)       // int
    timeout2 := parseTimeout("5m")      // string
    timeout3 := parseTimeout(3.14)      // float64, will use default
    timeout4 := parseTimeout("unknown") // unknown string, will use default

    fmt.Printf("Timeout 1: %d seconds\n", timeout1)
    fmt.Printf("Timeout 2: %d seconds\n", timeout2)
    fmt.Printf("Timeout 3: %d seconds\n", timeout3)
    fmt.Printf("Timeout 4: %d seconds\n", timeout4)
}

Output:

Timeout 1: 120 seconds
Timeout 2: 300 seconds
Timeout 3: 900 seconds
Timeout 4: 900 seconds

4. A Practical DevOps Use Case: Handling Different Error Types

Type switches are incredibly useful for more than just primitive types. A powerful pattern in Go is to use a type switch on an error interface value. A function might return different kinds of errors, each a struct with unique contextual information. The calling code can then inspect the error to decide how to react.

For example, a function that processes a file might return a "path error" if the file doesn't exist, or a "read error" if there's a problem during reading. A type switch lets you handle these cases differently.

The following video provides an excellent real-world example of this pattern.

Type Assertions in Go: the only guide you need!

The video 'Type Assertions in Go: the only guide you need!' by Flo Woelki demonstrates a practical scenario using type switches to handle different custom error types returned from a file processing function.

Watch the segment from 09:27 to 15:17. Focus on how the processFile function can return different error types (pathError vs. readError), and how the main function uses a type switch on the returned error to provide a specific message for each case.

This pattern of returning specific error types and using a type switch to handle them is fundamental to building robust automation tools in Go. It allows you to create much more informative logging and error handling logic than just dealing with a generic error string.


5. When to Use Which?

To summarize, the choice between a type assertion and a type switch is clear.

Go Type Assertion Example with io.Reader
The 'comma-ok' type assertion, as shown in the image above, is your tool for checking one type.
  • Use a two-value Type Assertion (v, ok := i.(T)) when you only care about one specific type. You're asking a yes/no question: "Does this interface value hold a string?"

  • Use a Type Switch (switch v := i.(type)) when you need to handle several possible types. You're asking a multiple-choice question: "Is this interface value a string, an int, a bool, or something else?"

This distinction is key to writing clean and idiomatic Go.


Conclusion

Today, you've added a powerful tool to your Go arsenal. You now know how to move beyond checking for a single type and can gracefully handle interfaces that might contain one of several different concrete types.

Key Takeaways:

  • A chain of if-else if statements with type assertions is functional but verbose.
  • A type switch is the idiomatic, readable, and maintainable way to handle an interface value that could be one of several different types.
  • The syntax switch v := i.(type) assigns the correctly-typed value to v within each case block.
  • Type switches are extremely useful for practical tasks like parsing dynamic configurations (JSON/YAML) and handling different categories of errors.

Next Up

So far, we've focused on consuming interfaces provided by Go, like error or the empty interface any. We've learned how to check what's inside them. In the next lesson, we will shift our focus to defining and implementing our own interfaces. This is where you'll unlock the full power of Go's approach to abstraction, enabling you to build flexible, decoupled, and highly testable applications.