32.7 When to Use Reflection and When to Avoid It
Look, let’s get one thing straight: reflection is like a fire axe behind glass that says “BREAK IN CASE OF EMERGENCY.” It’s an incredibly powerful tool that you should almost never use. The moment you reach for reflect, you’re saying, “I know the type system is there for a reason, but I’m going to ignore it for a bit.” It’s a deliberate step around the language’s safety features, and you’d better have a darn good reason for it.
The core trade-off is simple: you’re trading compile-time safety for runtime flexibility. The compiler, normally your best friend and a relentless nag about types, just throws its hands up and says, “Fine, you figure it out at runtime then.” This is why we use it sparingly, and only when the problem demands it.
The Unholy Trinity: Valid Reasons to Use Reflection
There are a few scenarios where the fire axe is actually the right tool for the job. These are the “emergencies.”
1. You’re Writing a Framework or Library That Must Handle Arbitrary Types. This is the big one. Think of JSON serialization libraries like encoding/json. I have no idea what struct you’re going to pass to json.Marshal, and I couldn’t care less. My job is to introspect your struct’s fields (looking for those json:"field_name" tags), figure out their types, and marshal the values accordingly. Without reflection, this is utterly impossible.
package main
import (
"encoding/json"
"fmt"
"reflect"
)
// A function that uses reflection to generically print struct field names.
// This is what a framework does.
func PrintFieldNames(s interface{}) {
t := reflect.TypeOf(s)
// Kind() tells us what we're dealing with (struct, slice, int, etc.)
if t.Kind() == reflect.Ptr {
// Dereference the pointer to get to the actual struct
t = t.Elem()
}
if t.Kind() != reflect.Struct {
panic("I only work with structs, sorry.")
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field %d: %s\n", i, field.Name)
}
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Alice", Age: 30}
PrintFieldNames(u)
// Output:
// Field 0: Name
// Field 1: Age
// And this is the more common, practical use:
jsonBytes, _ := json.Marshal(u)
fmt.Println(string(jsonBytes)) // {"name":"Alice","age":30}
}
2. You Need to Implement a Form of Ad-hoc Polymorphism. Sometimes, you need a function that can handle several fundamentally different types in a unified way. If they don’t share a common interface and you can’t modify their code, reflection might be your only escape hatch. It’s ugly, but it works.
3. For Deeply Introspective Debugging or Testing Tools. Writing a test assertion library that can compare two complex structs and tell you exactly which field differed? Yeah, you’re gonna need reflection for that. It’s the only way to peer into the values generically.
The Pit of Despair: Why You Should Avoid It
If the reasons to use it are specific, the reasons to avoid it are legion.
1. It’s Horrendous for Performance. Reflection is slow. It’s not a little slow; it’s orders-of-magnitude slow. Every call to reflect.TypeOf, ValueOf, FieldByName() involves runtime lookups and allocations that the compiler optimizes away for normal code. If this is in a hot path, you will notice.
2. It Turns Compile-Time Errors into Runtime Panics. This is the most important point. The compiler can’t save you here. Misspell a field name? That’s a panic. Try to SetInt on a string? That’s a panic. Try to call a method on a nil value? You get the idea. Your code becomes significantly more fragile and harder to reason about.
3. It’s Verbose and Complex. Compare a simple s.Name to the reflection equivalent: reflect.ValueOf(s).Elem().FieldByName("Name").String(). It’s a mess. It’s hard to write, and it’s even harder to read six months later.
4. It Often Breaks with go vet and Other Tools. The whole point of these tools is to catch problems reflection introduces. They’ll rightly complain about things like reflect.ValueOf(x).MethodByName("MyMethod").Call(...) because they can’t verify MyMethod actually exists.
The Golden Rule and Your Escape Hatch
Here’s your best practice, the one rule to rule them all: Always try to solve your problem with interfaces first. The type system is your friend. If you can define the behavior you need in an interface, that is always superior to using reflection.
If you absolutely must use reflection, contain it. Hide it behind a well-defined, non-reflective API. Write a function that uses reflection internally to figure something out, but returns results as standard types or expects inputs as interfaces. This isolates the nasty, panicky code into a small, tested compartment of your codebase, much like the standard library does.
For example, don’t make users call reflect methods. You call them for them and return a map[string]interface{} or a []string. You take the hit so they don’t have to.
Reflection is a tool for experts building infrastructure for everyone else. For 95% of your code, you should be a user of that infrastructure, not a builder of it. Know it exists, understand its power, and then politely close the glass case and walk away until you have no other choice.