13.5 Type Assertions vs Reflection: When to Use Each
Look, you’ve met interface{} (or its less shy alias, any). It’s the empty party that lets any type in. The real fun starts when you need to get a specific guest out of that party. You have two main tools for this: type assertions and the reflect package. One is a precise, lightweight scalpel; the other is a full surgical theater with a power plant attached. Knowing which to grab is the mark of a Go developer who doesn’t hate their CPU—or themselves.
The Scalpel: Type Assertions
A type assertion is your first, best choice. It’s a single, deliberate operation that checks if a value held by an interface is of a specific concrete type. It’s brutally efficient because it’s baked directly into the language’s runtime. Under the hood, it’s checking a type pointer. It’s about as fast as these things get.
You use it in two ways. The safe way, which tells you if it worked, and the “I’m-feeling-lucky” way, which panics if you’re wrong.
var myValue any = "I am a string"
// The safe way: check the 'ok'
str, ok := myValue.(string)
if ok {
fmt.Printf("Success! Value is: %s\n", str) // Prints: Success! Value is: I am a string
} else {
fmt.Println("Value was not a string")
}
// The "I'm-feeling-lucky" way: panic on failure
str := myValue.(string) // Fine
num := myValue.(int) // Panic: interface conversion: interface {} is string, not int
The rule is simple: Use the safe, two-value assignment unless you have absolute, 100%, iron-clad certainty of the type. That certainty almost only exists in the context of a type switch (which we’ll get to) or a tightly controlled package. If you’re getting data from the outside world (JSON, user input, a network call), you must use the safe form. A panic is not a control flow mechanism.
The Surgical Theater: The Reflect Package
Reflection is what you use when the scalpel isn’t enough. It’s for when you don’t just need to know if it’s a string, but you need to know if it’s a struct, how many fields it has, what their names and types are, and then maybe call a method by its string name. It’s for deep, dynamic, and often messy introspection.
import "reflect"
func investigate(v any) {
t := reflect.TypeOf(v)
fmt.Printf("Kind: %v, Name: %v\n", t.Kind(), t.Name())
if t.Kind() == reflect.Struct {
fmt.Println("It's a struct! Fields:")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(" %s: %v\n", field.Name, field.Type)
}
}
}
investigate("a string") // Kind: string, Name: string
investigate(struct{ Name string }{}) // Kind: struct, Name: ... Fields: Name: string
Reflection is incredibly powerful, but it’s also slow, verbose, and turns compile-time errors into runtime errors. The compiler can’t help you when you’re passing around reflect.Value objects.
So, Which Tool Do I Grab?
Here’s the simple heuristic: If you can do it with a type assertion or a type switch, you should. Full stop. The performance difference is not trivial; it’s monumental. Reflection should be your last resort, reserved for problems that are inherently about unknown types, not many known types.
- Use a type assertion when you have a strong expectation of one or two possible types. Checking for an
erroror extracting astringfrom a known map ofanyvalues are perfect examples. - Use a type switch when you have a closed set of several possible concrete types you need to handle. It’s a clean, efficient way to branch your logic.
func handleType(x any) {
switch x.(type) {
case string:
fmt.Println("It's a string!")
case int:
fmt.Println("It's an int!")
case nil:
fmt.Println("It's nil! Why would you do that?")
default:
fmt.Printf("No clue, boss. Type is %T\n", x)
}
}
- Use reflection only when the problem is fundamentally about the structure itself. Writing a generic JSON/YAML/TOML marshaller? That’s a job for
reflect. Building a framework that automatically maps HTTP form data to arbitrary user-provided structs? Yep, that’sreflect. Needing to check if something is a string? For the love of performance, use a type assertion.
The reflect package is the tool you break out when the problem is so complex that the verbosity and performance hit are worth the payoff. For 95% of your daily work with interfaces, the type assertion is your best friend. It’s direct, fast, and clearly communicates your intent.