How to Use Type Assertions in Go

Use the value.(type) syntax to safely extract a concrete type from an interface value in Go.

The universal remote problem

You are parsing a configuration file. The library returns a map of any. You grab a value, but the compiler only knows it's any. You need to treat it as a string or an int. You reach for a type assertion.

A type assertion is a runtime check. You ask the interface, "Do you hold a value of type T?" If yes, you get the value. If no, you get a failure or a panic.

Think of an interface value like a universal remote control. The remote can point at any device, but the buttons only work if you tell the remote what device you're targeting. Pressing "Volume Up" on a toaster does nothing useful. In Go, the remote panics if you try to control a toaster with TV commands, unless you check first.

Syntax and the comma-ok idiom

The syntax is value.(Type). You append the target type in parentheses to the variable.

There are two forms. The single-result form returns the value and panics if the type is wrong. The two-result form returns the value and a boolean. The boolean tells you if the assertion succeeded. The community calls this the "comma-ok" idiom. It appears everywhere in Go, not just type assertions.

Here's the safest way to assert a type. You use the two-result form to avoid panics.

package main

import "fmt"

func main() {
    // any is an alias for interface{}. It holds a value of any type.
    var box any = "hello"

    // Assert box contains a string. If true, msg gets the string value.
    // If false, msg is the zero value of string and ok is false.
    msg, ok := box.(string)

    if ok {
        fmt.Println("It's a string:", msg)
    } else {
        fmt.Println("Not a string")
    }
}

Type assertions are runtime checks. The compiler won't save you.

What happens under the hood

An interface value in Go is a pair of words: a type pointer and a data pointer. When you put an int into an any, the interface stores the type information for int and a pointer to the integer data.

A type assertion compares the stored type pointer with the type you requested. If they match, the assertion extracts the data pointer and returns it as the concrete type. If they don't match, the assertion fails.

This means type assertions do not convert. This trips up developers coming from Python or JavaScript. In Python, int("5") works. In Go, any("5").(int) panics. An assertion only succeeds if the underlying value is already an int. It extracts, it doesn't transform.

If you need conversion, you must assert to the source type first, then convert.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // The interface holds a string, not an int.
    var box any = "42"

    // Assert to string first. This succeeds because the underlying type is string.
    s, ok := box.(string)
    if !ok {
        fmt.Println("Not a string")
        return
    }

    // Convert the string to an int using strconv.
    // This is a conversion, not an assertion.
    n, err := strconv.Atoi(s)
    if err != nil {
        fmt.Println("Conversion failed:", err)
        return
    }

    fmt.Println("Integer value:", n)
}

Type assertions extract. Conversions transform. Know the difference.

Handling multiple types with a type switch

If you need to check for several types, a chain of type assertions gets messy. Go provides a type switch. It looks like a switch statement, but the expression is a type assertion with type as the target.

The type switch binds the concrete value to a new variable scoped to each case. This lets you use the value without repeating the assertion.

Here's how to handle a mixed bag of values cleanly. You use a type switch to branch on the concrete type.

package main

import "fmt"

// Describe prints a human-readable summary of a value stored in an interface.
func Describe(v any) {
    // Switch on the concrete type. The syntax v.(type) is only valid here.
    // Go binds the value to val with the correct type for each case.
    switch val := v.(type) {
    case string:
        fmt.Println("String:", val)
    case int:
        fmt.Println("Integer:", val)
    case bool:
        fmt.Println("Boolean:", val)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    Describe("test")
    Describe(42)
    Describe(true)
    Describe(3.14)
}

Prefer type switches over chains of assertions.

The typed nil trap

There is a subtle distinction between a nil interface and an interface holding a typed nil. This causes bugs that are hard to spot.

A nil interface has no type and no value. Both words in the interface pair are zero. An interface holding a typed nil has a type pointer but a nil data pointer.

When you assert, you are checking the type. If the interface holds a typed nil, the type matches, so the assertion succeeds and returns the nil pointer. If the interface is nil, the type is missing, so the assertion fails.

package main

import "fmt"

func main() {
    // s is a nil pointer of type *string.
    var s *string

    // box holds the type *string and a nil pointer.
    var box any = s

    // This assertion succeeds. The type matches.
    // p is a nil *string.
    p := box.(*string)
    fmt.Println("Assertion succeeded, pointer is nil:", p == nil)

    // empty is a nil interface. It has no type.
    var empty any

    // This panics. empty has no type, so it cannot be *string.
    // Uncommenting the next line causes a panic.
    // _ = empty.(*string)
}

If you uncomment the panic line, the runtime crashes with panic: interface conversion: interface is nil, not *string. The error message says "interface is nil," which means the interface value itself is nil, not just the data inside it.

Check for nil interfaces before asserting if the value might be uninitialized. Or use the comma-ok idiom to handle the failure gracefully.

A nil interface is not the same as a typed nil. Check the interface first.

When type assertions leak abstraction

Type assertions are useful when you interact with external data, like JSON or configuration maps. They are less useful when you design your own APIs.

If a function returns any, the caller must assert to use the result. This pushes type safety to the caller and makes the code fragile. If the function changes the internal type, every caller breaks at runtime.

The convention in Go is to return concrete types or interfaces that define behavior, not any. If you find yourself writing type assertions in your own code, ask why the type was lost. You might need a struct, a specific interface, or generics.

Generics let you constrain types at compile time. If you write a function that works on any type but needs to preserve the type, use a type parameter instead of any.

package main

import "fmt"

// PrintFirst takes a slice of any type T and prints the first element.
// The type parameter T preserves the type information at compile time.
// No type assertions are needed.
func PrintFirst[T any](s []T) {
    if len(s) > 0 {
        fmt.Println("First:", s[0])
    }
}

func main() {
    PrintFirst([]string{"hello", "world"})
    PrintFirst([]int{1, 2, 3})
}

If you're asserting, ask why the type was lost in the first place.

Pitfalls and compiler errors

Type assertions happen at runtime. The compiler cannot verify them. You must handle errors yourself.

The single-result form panics on failure. Use it only when you are certain of the type. The two-result form is safer. Use it when the type might vary.

If you forget to check the boolean in the comma-ok idiom, you might use a zero value without realizing the assertion failed. The compiler won't warn you. The variable is initialized to the zero value of the target type.

Common runtime panics include:

  • panic: interface conversion: any is string, not int when you assert the wrong type.
  • panic: interface conversion: interface is nil, not string when you assert on a nil interface.
  • panic: interface conversion: any is *main.MyStruct, not main.MyStruct when you mix pointers and values. Asserting MyStruct on an interface holding *MyStruct fails. The types must match exactly.

Type assertions are runtime checks. The compiler won't save you.

Decision matrix

Use a type assertion when you have an any value from external code and need to recover the concrete type for specific logic.

Use a type switch when you need to handle multiple possible types in one block.

Use generics when you can constrain the type at compile time instead of checking at runtime.

Use a struct with fields when the data has a known shape, avoiding the need for dynamic inspection.

Use the comma-ok idiom when the type might be wrong and you want to handle the failure gracefully.

Trust the comma-ok idiom. It makes the unhappy path visible.

Where to go next