Skip to Content
Course content

240: Type Switches on Interfaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR from a junior dev who was building a message dispatcher for a distributed system. He had this massive block of code—nearly a hundred lines—consisting of a repetitive chain of if v, ok := msg.(UserCreated); ok { ... } else if v, ok := msg.(OrderPlaced); ok { ... }. It was a "type assertion ladder," and it was a nightmare to read. Every time he added a new event type, the ladder grew, and the indentation started drifting toward the right side of the screen. I told him, "You're doing the right thing logically, but you're fighting the language." In Go, when you find yourself asserting the same interface over and over, you should be using a type switch.

Moving Beyond Manual Type Assertions

You already know that a type assertion allows you to extract the concrete value from an interface. But when a single interface variable could realistically be one of five or ten different types, those if-ok blocks become noise. A type switch is essentially syntactic sugar that tells the Go compiler: "I know this is an interface, but I want to perform different logic based on what the concrete type actually is."

The magic happens with the .(type) expression. Note that this syntax is only valid inside a switch statement. If you try to use .(type) in an if block, the compiler will yell at you. It’s a specialized construct designed specifically for this pattern.

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

type Square struct {
    Side float64
}

func (c Circle) Area() float64 { return 3.14 * c.Radius * c.Radius }
func (s Square) Area() float64 { return s.Side * s.Side }

func DescribeShape(s Shape) {
    // Here is the type switch
    switch v := s.(type) {
    case Circle:
        fmt.Printf("This is a circle with radius %.2f\n", v.Radius)
    case Square:
        fmt.Printf("This is a square with side %.2f\n", v.Side)
    default:
        fmt.Println("Unknown shape type")
    }
}

Capturing the Concrete Value

Look closely at the line switch v := s.(type). This is the most critical part of the pattern. By assigning the result to v, Go automatically casts v to the correct concrete type within each case block. Inside the case Circle block, v isn't a Shape anymore—it's a Circle. You get full access to the Radius field without having to perform another manual assertion.

I often see developers forget the assignment and just write switch s.(type). While that works if you only care about the type and not the value, you'll almost always need the value to actually do anything useful. Always use the v := ... form unless you have a very specific reason not to.

Handling Multiple Types in One Case

Sometimes, you don't need different logic for every single type; you just need to know if the value belongs to a specific group of types. You can comma-separate types in a single case. However, there is a catch: if you group types, you cannot use the assigned variable v as a concrete type because the compiler doesn't know which one of the group v actually is. In those cases, v remains the original interface type.

switch v := s.(type) {
case Circle, Square:
    // v is still of type Shape here, not Circle or Square
    fmt.Printf("This is a geometric shape with area %.2f\n", v.Area())
case string:
    fmt.Println("Someone passed a string instead of a Shape!")
default:
    fmt.Println("Totally unsupported type")
}

I personally find that using the default case is non-negotiable here. Since interfaces are open—meaning anyone can implement them—you can never be 100% sure that a new type won't be passed into your function in the future. The default case is your safety net against panics or silent failures.




📋 Practical Task

Exercise: Implementing a Multi-Format Event Processor

You are building a telemetry system that receives data from various sources. Some sources send data as simple strings, some as integers (representing status codes), and some as a structured TelemetryEvent struct. Your goal is to process these diverse types using a single handler function.

Requirements:

  • Define a struct called TelemetryEvent with two fields: Source string and Value float64.
  • Write a function called ProcessData(data any) (using the any alias for interface{}).
  • Inside ProcessData, use a type switch to handle the following:
    • If the data is a string, print: "Log Message: [string value]".
    • If the data is an int, print: "Status Code: [int value]".
    • If the data is a TelemetryEvent, print: "Event from [Source]: [Value]".
    • For any other type, print: "Unknown data format received".
  • In your main function, create a slice of any containing one of each of the above types, plus one unsupported type (like a bool), and loop through them calling ProcessData for each.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.