Go Reflection: When Your Code Needs to Examine Itself
The Framework Developer's Problem
You're building a JSON library. Users give you any struct. You need to read its fields, check their types, and convert values to JSON. But you don't know the struct at compile time. How do you read fields of an unknown type?
Or you're building a validation library. Users tag their struct fields with rules like
validate:"required,min=5". You need to read those tags and validate accordingly. At compile time, you don't know what structs users will create.This is where reflection comes in. It lets your code examine and manipulate itself at runtime.
Why Static Typing Creates This Problem
Go is statically typed. The compiler knows every type at compile time. This gives us safety and performance. But it also means we can't write truly generic code that handles any type.

Go blog diagram 1
Reflection breaks this wall. It lets us inspect types at runtime, read struct fields we don't know about, and call methods dynamically.
The Mirror Analogy
Think of it like this: Reflection is like looking in a mirror to examine yourself. Normally, you just act. With a mirror, you can see what you look like, count your fingers, check your clothes. Reflection lets your code do the same. Instead of just working with values, it can step back and ask "What type is this? What fields does it have? What methods can I call?"
The Two Pillars: Type and Value
Everything in reflection revolves around two concepts:
- Type: What kind of thing is this? (struct, int, slice, etc.)
- Value: What is the actual data?
go// Filename: reflection_basics.go package main import ( "fmt" "reflect" ) type Person struct { Name string Age int } func main() { p := Person{Name: "Alice", Age: 30} // Get the type t := reflect.TypeOf(p) fmt.Println("Type:", t) // main.Person fmt.Println("Kind:", t.Kind()) // struct // Get the value v := reflect.ValueOf(p) fmt.Println("Value:", v) // {Alice 30} // Examine fields fmt.Println("\nFields:") for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) fmt.Printf(" %s (%s) = %v\n", field.Name, field.Type, value) } }
Expected Output:
Type: main.Person Kind: struct Value: {Alice 30} Fields: Name (string) = Alice Age (int) = 30

Go blog diagram 2
Kind vs Type
Type is the specific named type. Kind is the underlying category.
go// Filename: kind_vs_type.go package main import ( "fmt" "reflect" ) type MyInt int type MyString string func main() { var a MyInt = 42 var b MyString = "hello" var c int = 42 var d []int = []int{1, 2, 3} // MyInt: Type is MyInt, Kind is int fmt.Printf("MyInt: Type=%v, Kind=%v\n", reflect.TypeOf(a), reflect.TypeOf(a).Kind()) // MyString: Type is MyString, Kind is string fmt.Printf("MyString: Type=%v, Kind=%v\n", reflect.TypeOf(b), reflect.TypeOf(b).Kind()) // int: Type and Kind are both int fmt.Printf("int: Type=%v, Kind=%v\n", reflect.TypeOf(c), reflect.TypeOf(c).Kind()) // []int: Type is []int, Kind is slice fmt.Printf("[]int: Type=%v, Kind=%v\n", reflect.TypeOf(d), reflect.TypeOf(d).Kind()) }
Expected Output:
MyInt: Type=main.MyInt, Kind=int MyString: Type=main.MyString, Kind=string int: Type=int, Kind=int []int: Type=[]int, Kind=slice
Reading Struct Tags
Struct tags are metadata attached to fields. Reflection is the only way to read them.
go// Filename: struct_tags.go package main import ( "fmt" "reflect" ) type User struct { ID int `json:"id" db:"user_id"` Name string `json:"name" validate:"required,min=2"` Email string `json:"email" validate:"required,email"` Password string `json:"-"` // Ignored in JSON } func main() { t := reflect.TypeOf(User{}) fmt.Println("Field tags:") for i := 0; i < t.NumField(); i++ { field := t.Field(i) fmt.Printf("\n%s:\n", field.Name) fmt.Printf(" json: %s\n", field.Tag.Get("json")) fmt.Printf(" db: %s\n", field.Tag.Get("db")) fmt.Printf(" validate: %s\n", field.Tag.Get("validate")) } }
Expected Output:
Field tags: ID: json: id db: user_id validate: Name: json: name db: validate: required,min=2 Email: json: email db: validate: required,email Password: json: - db: validate:
Modifying Values with Reflection
To modify values, you must pass pointers and use
Elem().go// Filename: modify_values.go package main import ( "fmt" "reflect" ) type Config struct { Host string Port int } func main() { cfg := Config{Host: "localhost", Port: 8080} fmt.Println("Before:", cfg) // Must use pointer to modify v := reflect.ValueOf(&cfg).Elem() // Check if we can set values fmt.Println("CanSet:", v.CanSet()) // true // Modify Host field hostField := v.FieldByName("Host") if hostField.CanSet() { hostField.SetString("production.example.com") } // Modify Port field portField := v.FieldByName("Port") if portField.CanSet() { portField.SetInt(443) } fmt.Println("After:", cfg) }
Expected Output:
Before: {localhost 8080} CanSet: true After: {production.example.com 443}

Go blog diagram 3
Real World Example: Simple Validator
go// Filename: validator.go package main import ( "fmt" "reflect" "strings" ) // Validate checks struct fields against their validate tags func Validate(v interface{}) []string { var errors []string val := reflect.ValueOf(v) typ := reflect.TypeOf(v) // Handle pointer if val.Kind() == reflect.Ptr { val = val.Elem() typ = typ.Elem() } for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) value := val.Field(i) tag := field.Tag.Get("validate") if tag == "" { continue } rules := strings.Split(tag, ",") for _, rule := range rules { if rule == "required" { if isZero(value) { errors = append(errors, fmt.Sprintf("%s is required", field.Name)) } } if strings.HasPrefix(rule, "min=") { minLen := 0 fmt.Sscanf(rule, "min=%d", &minLen) if value.Kind() == reflect.String && value.Len() < minLen { errors = append(errors, fmt.Sprintf("%s must be at least %d characters", field.Name, minLen)) } } } } return errors } func isZero(v reflect.Value) bool { switch v.Kind() { case reflect.String: return v.Len() == 0 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Bool: return !v.Bool() default: return false } } type User struct { Name string `validate:"required,min=2"` Email string `validate:"required"` Age int `validate:"required"` } func main() { // Valid user user1 := User{Name: "Alice", Email: "alice@example.com", Age: 30} errors := Validate(user1) fmt.Println("User1 errors:", errors) // Invalid user user2 := User{Name: "A", Email: "", Age: 0} errors = Validate(user2) fmt.Println("User2 errors:", errors) }
Expected Output:
User1 errors: [] User2 errors: [Name must be at least 2 characters Email is required Age is required]
Calling Methods Dynamically
go// Filename: call_methods.go package main import ( "fmt" "reflect" ) type Calculator struct { Value int } func (c *Calculator) Add(n int) int { c.Value += n return c.Value } func (c *Calculator) Multiply(n int) int { c.Value *= n return c.Value } func main() { calc := &Calculator{Value: 10} // Get value of calculator v := reflect.ValueOf(calc) // Get Add method addMethod := v.MethodByName("Add") // Call Add(5) args := []reflect.Value{reflect.ValueOf(5)} result := addMethod.Call(args) fmt.Println("After Add(5):", result[0].Int()) // Call Multiply(3) multiplyMethod := v.MethodByName("Multiply") result = multiplyMethod.Call([]reflect.Value{reflect.ValueOf(3)}) fmt.Println("After Multiply(3):", result[0].Int()) }
Expected Output:
After Add(5): 15 After Multiply(3): 45
Creating Values Dynamically
go// Filename: create_values.go package main import ( "fmt" "reflect" ) type Person struct { Name string Age int } func main() { // Get type of Person personType := reflect.TypeOf(Person{}) // Create new Person instance newPerson := reflect.New(personType).Elem() // Set fields newPerson.FieldByName("Name").SetString("Bob") newPerson.FieldByName("Age").SetInt(25) // Convert to actual Person person := newPerson.Interface().(Person) fmt.Printf("Created: %+v\n", person) // Create a slice dynamically sliceType := reflect.SliceOf(personType) slice := reflect.MakeSlice(sliceType, 0, 10) // Append to slice slice = reflect.Append(slice, reflect.ValueOf(Person{Name: "Alice", Age: 30})) slice = reflect.Append(slice, reflect.ValueOf(person)) fmt.Println("Slice:", slice.Interface()) }
Expected Output:
Created: {Name:Bob Age:25} Slice: [{Alice 30} {Bob 25}]
Performance Cost of Reflection
Reflection is slow compared to direct access. Use it for setup, not hot paths.

Go blog diagram 4
| Operation | Direct | Reflection | Slowdown |
|---|---|---|---|
| Field read | 1ns | 50-100ns | 50-100x |
| Method call | 2ns | 100-200ns | 50-100x |
| New instance | 5ns | 200-500ns | 40-100x |
When to Use Reflection
Good Use Cases:
- JSON/XML marshaling
- ORM field mapping
- Validation frameworks
- Dependency injection
- Configuration loading
- Testing utilities
Avoid Reflection For:
- Performance critical code
- Logic that can use generics
- Simple type switches
- Code that can use interfaces
go// WRONG: Using reflection when unnecessary func PrintName(v interface{}) { val := reflect.ValueOf(v) name := val.FieldByName("Name") fmt.Println(name.String()) } // RIGHT: Use an interface type Named interface { GetName() string } func PrintName(n Named) { fmt.Println(n.GetName()) }
Common Mistakes
Mistake 1: Forgetting to use Elem()
go// WRONG: Can't set because it's not addressable v := reflect.ValueOf(myStruct) v.FieldByName("Name").SetString("new") // Panic! // RIGHT: Use pointer and Elem() v := reflect.ValueOf(&myStruct).Elem() v.FieldByName("Name").SetString("new")
Mistake 2: Not checking CanSet()
go// WRONG: Assuming we can always set field.SetString("value") // Might panic // RIGHT: Always check if field.CanSet() { field.SetString("value") }
Mistake 3: Wrong type assertions
go// WRONG: Panics if wrong type value.Interface().(string) // RIGHT: Use type switch or comma-ok if str, ok := value.Interface().(string); ok { // use str }
What You Learned
You now understand that:
- TypeOf gives metadata: Field names, types, tags
- ValueOf gives data: Actual values, ability to modify
- Elem() dereferences pointers: Required for modification
- CanSet() prevents panics: Always check before setting
- Reflection is slow: Use for initialization, not hot paths
- Struct tags need reflection: The only way to read them
Your Next Steps
- Build: Create a simple struct-to-map converter
- Read Next: Explore how encoding/json uses reflection
- Experiment: Build a dependency injection container
Reflection is powerful but comes with costs. Use it when you truly need runtime introspection. For everything else, interfaces and generics are faster and safer. When you do need reflection, now you know how to wield it properly.