When you need to inspect a struct at runtime
You're building a logger that needs to dump every field of a request object. Or a validator that checks if any string field is empty. Writing a manual switch statement for every struct type gets repetitive fast. You want a single function that accepts any struct and iterates over its fields automatically. Go's type system is static, but the reflect package gives you a way to inspect types and values while the program runs.
Reflection is like holding a mirror up to your code. Normally, the compiler knows everything about your types before the program starts. Reflection lets the program ask questions about itself during execution. You can ask "What is this type?", "How many fields does it have?", and "What is the value of the third field?". The reflect package wraps any value in a reflect.Value and any type in a reflect.Type. These wrappers expose methods to drill down into the structure.
The minimal loop
Here's the simplest way to iterate over a struct's fields: wrap the value, get the type, count the fields, and loop.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
// Wrap the value so reflection can inspect it
v := reflect.ValueOf(p)
// Get the type description to query field count and names
t := v.Type()
// Loop over the number of fields defined in the struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
// Interface() extracts the actual value from the reflection wrapper
fmt.Printf("%s: %v\n", field.Name, value.Interface())
}
}
What happens under the hood
When you call reflect.ValueOf(p), you're boxing the Person value into a reflection wrapper. The wrapper holds the data and metadata. v.Type() returns the reflect.Type which describes the shape. t.NumField() returns an integer count. Inside the loop, t.Field(i) gives you metadata (name, type, tags), while v.Field(i) gives you the runtime value. Calling Interface() on the value unwraps it back to an interface{} so you can print it or use it.
This round-trip through reflection is where the performance cost lives. The compiler can't optimize these calls because the type isn't known at compile time. Every call to Interface() allocates a new interface value. In a tight loop, this generates garbage. Reflection is a tool for building libraries, not for hot paths in business logic.
You'll also see Kind() used in reflection code. Type tells you the specific type, like Person. Kind tells you the underlying type, like Struct, Slice, or Int. Kind is useful for writing generic code that handles different types. A Person has kind Struct. A []Person has kind Slice.
Realistic example: struct to map with tags
Here's a common pattern: converting a struct to a map, using struct tags to control the keys. Tags are metadata attached to fields, often used by encoding/json.
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int `json:"id"`
Email string `json:"email"`
// Unexported fields are invisible to reflection
secret string
}
// StructToMap converts a struct to a map using json tags as keys
func StructToMap(s interface{}) map[string]interface{} {
v := reflect.ValueOf(s)
t := v.Type()
result := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
// Use the json tag if present, otherwise fall back to field name
key := field.Tag.Get("json")
if key == "" {
key = field.Name
}
// Only add if the field is valid and can be read
if value.IsValid() {
result[key] = value.Interface()
}
}
return result
}
func main() {
u := User{ID: 1, Email: "alice@example.com", secret: "hidden"}
m := StructToMap(u)
fmt.Printf("%+v\n", m)
}
The Tag.Get("json") method parses the tag string and returns the value for the json key. If the tag is missing, it returns an empty string. This lets you customize the output without changing the struct definition. Tags are metadata, not magic. The runtime doesn't enforce them. You have to write the code to read them.
Notice the secret field. It starts with a lowercase letter. In Go, public names start with a capital letter. Private start lowercase. Reflection respects Go's visibility rules. You can't peek at private fields from outside the package. v.Field(i) returns a value for secret, but you can't set it. Attempting to do so triggers a panic.
Nested structs and FieldByIndex
Structs often contain other structs. If you have an Address inside Person, Field(i) returns the Address struct value. You can't just loop top-level fields if you want to flatten the data. You need to check if a field is a struct and recurse.
// ... inside a loop ...
field := t.Field(i)
value := v.Field(i)
// Check if the field is a struct to handle nesting
if field.Type.Kind() == reflect.Struct {
// Recurse or handle nested struct logic here
}
For deeply nested fields, you can use FieldByIndex. It takes a slice of indices and drills down. If Address is at index 1, and Street is at index 0 inside Address, you can access it directly.
// Access Street inside Address without recursion
street := v.FieldByIndex([]int{1, 0})
This is faster than recursion if you know the path. It's also how encoding/json handles nested objects.
Pitfalls and runtime errors
Reflection breaks compile-time safety. The compiler can't check your reflection code. Errors happen at runtime.
Pointers vs values. If you pass a struct value instead of a pointer, the reflection wrapper holds a copy. Trying to modify fields will fail. The runtime panics with reflect: Set of unaddressable value. You must pass a pointer and call .Elem() to get the underlying struct.
// Pass pointer to allow modification
v := reflect.ValueOf(&p)
// Elem() dereferences the pointer to get the struct value
s := v.Elem()
Unexported fields. v.Field(i) returns a value, but you cannot set it. Attempting to do so triggers reflect: Set of unexported field. The compiler won't catch this. The crash happens at runtime. Always check field.IsExported() or value.CanSet() before trying to modify.
Interface conversion. Interface() can fail if the value is invalid. The runtime panics with reflect: call of reflect.Value.Interface on zero Value. Check value.IsValid() first.
Performance. Reflection is slow. It involves dynamic dispatch, interface conversions, and allocation. Don't use reflection in hot loops. Use it for initialization, logging, or serialization where the cost is amortized.
Reflection is a tool, not a default. Trust the compiler over runtime checks.
When to use reflection
Use reflection when you need generic utilities like serialization, logging, or validation that must work on arbitrary types without code generation. Use code generation with go generate when you need high performance and compile-time safety, as generated code expands to direct field access. Use manual mapping or switch statements when you only handle a few known types, keeping the code readable and fast. Use interfaces when you can define a contract that types implement, avoiding reflection entirely by relying on polymorphism. Use the encoding/json package when you just need standard serialization, as it handles reflection internally and optimizes common patterns.
Accept interfaces, return structs. Reflection accepts interface{} and returns concrete values. This fits the Go style mantra. Don't fight the type system. Wrap the value or change the design.