The generic tool problem
You are building a logging library that prints the contents of any struct passed to it. You don't know the struct type at compile time. The caller might pass a User, a Config, or a DatabaseConnection. Your function signature takes any, but once the value is inside, the type information is gone. You can't access fields because the compiler refuses to let you touch data it doesn't understand.
Or you are writing a framework that needs to inject dependencies into fields based on tags. You need to find a field named Logger, check if it's set, and assign a value if it's empty. The type system locks you out. You need to reach inside the box without knowing what's inside.
Reflection is the tool for that. It lets the program inspect and modify values while running, using type information that was baked in but usually hidden.
Reflection: opening the hood
Go is statically typed. The compiler knows every type and field before the program runs. This gives you speed and safety. Reflection breaks that rule. It exposes the type system to the running program.
Think of it like a mechanic's manual for a car. Normally, you just drive the car. You press the gas, the car goes. You don't care about the fuel injectors. Reflection is opening the hood and looking at the wiring diagram while the engine is running. You can read the voltage on a sensor or swap a part, but you have to know exactly how to read the diagram, and you risk shorting something out if you're careless.
The reflect package revolves around two types: reflect.Type and reflect.Value. Type describes the shape: field names, types, methods. Value holds the actual data. When you call reflect.ValueOf, you get a box containing your data. If you pass a pointer, the box contains a pointer. You must call .Elem() to step inside the pointer and get the struct. Without .Elem(), you're holding a pointer to the struct, not the struct itself, and you can't access fields.
Go's visibility rules apply to reflection. FieldByName only sees exported fields. If a field starts with a lowercase letter, reflection treats it as invisible. This preserves encapsulation. You can't use reflection to hack into private fields of a library. The convention holds: public names start with a capital letter, private start lowercase. Reflection respects that boundary.
Reflection is a sledgehammer. Use it to break walls, not to crack nuts.
Minimal example
Here's the core pattern: get a reflect.Value, unwrap pointers, find the field, read or write.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
// Pass a pointer so reflection can modify the original value.
p := &Person{Name: "Alice", Age: 30}
// Elem() unwraps the pointer to get the struct value itself.
// Without this, you hold a pointer, not the struct.
v := reflect.ValueOf(p).Elem()
// FieldByName returns a Value representing the field.
// It returns a zero Value if the name doesn't exist.
nameField := v.FieldByName("Name")
// String() extracts the underlying string from the reflect.Value.
fmt.Println("Name:", nameField.String())
// SetInt writes a new value to the field.
// This only works if the field is settable and exported.
v.FieldByName("Age").SetInt(35)
fmt.Println("Age:", p.Age)
}
How the types work
The reflect package distinguishes between Kind and Type. Kind is the underlying category: Struct, Slice, Map, Pointer, Int. Type is the specific definition: Person, []byte, map[string]int.
If you have a []byte, the Kind is Slice and the Type is []byte. If you have a []int, the Kind is still Slice, but the Type is []int. You use Kind to decide how to handle a value generically. You use Type to check exact matches.
When iterating over fields, you often check Kind to determine which setter to call. SetString works for string. SetInt works for int, int8, int16, int32, int64. SetBool works for bool. Calling the wrong setter panics. The compiler rejects this with reflect: call of reflect.Value.SetString on Value of type int. You must match the kind.
FieldByName returns a Value. If the field doesn't exist, it returns a zero Value. You should check .IsValid(). If you try to call .String() on an invalid value, it panics. The panic message is reflect: call of reflect.Value.String on zero Value. Always check validity before using a field.
Setting a field requires two conditions. The field must be exported. The struct must be addressable. If you pass a struct by value, the reflection sees a copy. Modifying the copy does nothing useful. The compiler rejects v.Set(...) with reflect: reflect.Value.Set using unaddressable value if the underlying value isn't addressable. Always pass pointers to structs when you want to modify them.
The reflect package prefers panics over errors for misuse. Functions rarely return errors. They return zero values or panic. You must check validity yourself. This design keeps the API compact but shifts the burden to the caller. Validate early.
Realistic usage
Here's a helper function that safely sets a field by name, handling common errors. It checks types at runtime and returns an error instead of panicking.
package main
import (
"fmt"
"reflect"
)
type Server struct {
Host string
Port int
}
// SetField updates a struct field by name with runtime type checking.
// It returns an error if the field is missing, unexported, or type-mismatched.
func SetField(s interface{}, name string, value interface{}) error {
// Elem() dereferences the pointer to access the struct fields.
v := reflect.ValueOf(s).Elem()
// FieldByName returns a zero Value if the name is invalid.
f := v.FieldByName(name)
if !f.IsValid() {
return fmt.Errorf("field %s not found", name)
}
// CanSet() checks if the field is exported and the struct is addressable.
if !f.CanSet() {
return fmt.Errorf("field %s is not settable", name)
}
// Types must match exactly; reflection does not auto-convert.
if f.Type() != reflect.TypeOf(value) {
return fmt.Errorf("type mismatch: expected %s, got %s", f.Type(), reflect.TypeOf(value))
}
// Set copies the value from the reflect.Value into the field.
f.Set(reflect.ValueOf(value))
return nil
}
Here's how you call it. Notice the pointer usage.
func main() {
srv := &Server{Host: "localhost", Port: 80}
// Pass a pointer so the struct is addressable.
err := SetField(srv, "Port", 8080)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Port is now:", srv.Port)
}
}
Pitfalls and panics
Reflection is slow. It bypasses compiler optimizations. The compiler can't inline reflection calls. It can't prove types at compile time. Reflection can be 10x to 100x slower than direct access. Use it sparingly. Cache reflect.Type and reflect.Value of fields if you need to access them repeatedly. Creating a reflect.Value allocates memory.
reflect.Value is not comparable. You can't put reflect.Value in a map key. You can't compare two Values with ==. Use reflect.DeepEqual for comparison, but be aware it's also slow and can panic on recursive structures.
The worst reflection bug is the silent failure. FieldByName returns a zero value for missing fields. If you forget to check IsValid, you might call Set on a zero value. The program panics at runtime. In production, this crashes the server. Always check IsValid and CanSet.
Reflection hides mistakes until runtime. The compiler is your friend. It catches typos in field names and type mismatches before the code runs. Reflection removes those guarantees. Only use it when you truly need dynamic behavior.
Generics replaced many reflection use cases in Go 1.18. Check if generics solve your problem first. Generics give you compile-time safety with near-zero overhead. Reflection gives you flexibility with cost and risk.
Decision matrix
Use reflection when you are building a framework that must handle arbitrary types, like a JSON encoder or an ORM.
Use reflection when you need to inspect struct tags to drive behavior, such as validation or dependency injection.
Use interface{} with type switches when you have a small, known set of types and performance matters.
Use generics when you can constrain types to a set and want compile-time safety with minimal overhead.
Use plain functions when the type is fixed; reflection adds complexity and cost with no benefit.
Generics first. Reflection last.