Go Reflection: reflect Package Guide

Reflection in Go provides the ability to inspect and manipulate types and values at runtime. While Go is a statically-typed language, the `reflect` package offers an escape hatch for scenarios where...

Key Insights

  • Reflection enables runtime type inspection and manipulation but comes with significant performance overhead—use it only when static typing is impossible
  • The reflect.Type and reflect.Value pair forms the foundation of Go’s reflection system, requiring careful handling of addressability and settability rules
  • Real-world reflection use cases like struct tag parsing and dynamic function invocation power critical tools like JSON marshalers and dependency injection frameworks

Introduction to Reflection in Go

Reflection in Go provides the ability to inspect and manipulate types and values at runtime. While Go is a statically-typed language, the reflect package offers an escape hatch for scenarios where you need to work with types that aren’t known until runtime.

The standard library uses reflection extensively. When you call json.Marshal(), reflection examines your struct’s fields and tags to produce JSON. ORMs like GORM use reflection to map database rows to struct fields. Dependency injection frameworks rely on reflection to wire dependencies automatically.

However, reflection is not free. It’s significantly slower than static type operations, harder to read, and bypasses compile-time type safety. Use reflection only when necessary—typically for building libraries and frameworks rather than application code.

Here’s a simple comparison:

// Static approach - fast, type-safe
type User struct {
    Name string
    Age  int
}

func printUserStatic(u User) {
    fmt.Printf("Name: %s, Age: %d\n", u.Name, u.Age)
}

// Reflection approach - flexible but slower
func printUserReflection(i interface{}) {
    v := reflect.ValueOf(i)
    t := v.Type()
    
    for i := 0; i < v.NumField(); i++ {
        fmt.Printf("%s: %v\n", t.Field(i).Name, v.Field(i).Interface())
    }
}

Core Reflection Concepts: Type and Value

The reflect package centers around two fundamental types: reflect.Type and reflect.Value. Understanding these is essential for effective reflection usage.

reflect.Type represents a Go type and provides metadata about it. You obtain a Type using reflect.TypeOf():

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    
    t := reflect.TypeOf(p)
    fmt.Println("Type:", t)           // Type: main.Person
    fmt.Println("Kind:", t.Kind())    // Kind: struct
    fmt.Println("Name:", t.Name())    // Name: Person
    fmt.Println("Size:", t.Size())    // Size: 24 (on 64-bit)
    
    // For basic types
    var x int = 42
    xt := reflect.TypeOf(x)
    fmt.Println("Kind:", xt.Kind())   // Kind: int
    fmt.Println("Name:", xt.Name())   // Name: int
}

reflect.Value represents a runtime value and allows you to read and potentially modify it:

func main() {
    p := Person{Name: "Bob", Age: 25}
    
    v := reflect.ValueOf(p)
    fmt.Println("Value:", v)                    // Value: {Bob 25}
    fmt.Println("Type:", v.Type())              // Type: main.Person
    fmt.Println("Kind:", v.Kind())              // Kind: struct
    fmt.Println("Interface:", v.Interface())    // Interface: {Bob 25}
}

The Kind() method distinguishes between the underlying type category (struct, int, slice, etc.) and the named type. This is crucial when writing generic reflection code.

Inspecting Struct Fields and Tags

Struct reflection is one of the most common use cases. You can iterate over fields, read their properties, and extract struct tags:

type User struct {
    ID        int    `json:"id" validate:"required"`
    Username  string `json:"username" validate:"required,min=3"`
    Email     string `json:"email" validate:"email"`
    IsActive  bool   `json:"is_active"`
}

func inspectStruct(i interface{}) {
    t := reflect.TypeOf(i)
    v := reflect.ValueOf(i)
    
    if t.Kind() != reflect.Struct {
        fmt.Println("Not a struct")
        return
    }
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        
        fmt.Printf("Field: %s\n", field.Name)
        fmt.Printf("  Type: %s\n", field.Type)
        fmt.Printf("  Value: %v\n", value.Interface())
        fmt.Printf("  JSON tag: %s\n", field.Tag.Get("json"))
        fmt.Printf("  Validate tag: %s\n", field.Tag.Get("validate"))
        fmt.Println()
    }
}

Here’s a practical struct-to-map converter that respects JSON tags:

func structToMap(i interface{}) map[string]interface{} {
    result := make(map[string]interface{})
    
    v := reflect.ValueOf(i)
    t := reflect.TypeOf(i)
    
    for i := 0; i < v.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        
        // Use JSON tag if available, otherwise use field name
        key := field.Tag.Get("json")
        if key == "" {
            key = field.Name
        }
        
        result[key] = value.Interface()
    }
    
    return result
}

// Usage
user := User{ID: 1, Username: "alice", Email: "alice@example.com", IsActive: true}
m := structToMap(user)
fmt.Println(m) // map[email:alice@example.com id:1 is_active:true username:alice]

Modifying Values with Reflection

Reading values is straightforward, but modifying them requires understanding addressability and settability. A reflect.Value is settable only if it represents an addressable location in memory:

func modifyValues() {
    x := 42
    
    // This won't work - value is not addressable
    v := reflect.ValueOf(x)
    fmt.Println("CanSet:", v.CanSet()) // false
    // v.SetInt(100) // This would panic!
    
    // Pass a pointer to make it addressable
    v = reflect.ValueOf(&x).Elem()
    fmt.Println("CanSet:", v.CanSet()) // true
    v.SetInt(100)
    fmt.Println("x:", x) // 100
}

The Elem() method dereferences pointers and interfaces. This is essential when working with pointer receivers:

func setStructFields(i interface{}) error {
    v := reflect.ValueOf(i)
    
    // Must be a pointer to a struct
    if v.Kind() != reflect.Ptr {
        return fmt.Errorf("expected pointer, got %v", v.Kind())
    }
    
    v = v.Elem()
    if v.Kind() != reflect.Struct {
        return fmt.Errorf("expected struct, got %v", v.Kind())
    }
    
    // Set fields by name
    usernameField := v.FieldByName("Username")
    if usernameField.IsValid() && usernameField.CanSet() {
        usernameField.SetString("modified_user")
    }
    
    return nil
}

// Usage
user := &User{Username: "original"}
setStructFields(user)
fmt.Println(user.Username) // modified_user

Working with Functions and Methods

Reflection can inspect and invoke functions dynamically. This powers dependency injection containers and testing frameworks:

func add(a, b int) int {
    return a + b
}

func callFunction() {
    fn := reflect.ValueOf(add)
    
    // Check if it's a function
    if fn.Kind() != reflect.Func {
        panic("not a function")
    }
    
    // Inspect function signature
    fnType := fn.Type()
    fmt.Printf("NumIn: %d, NumOut: %d\n", fnType.NumIn(), fnType.NumOut())
    
    // Call the function
    args := []reflect.Value{
        reflect.ValueOf(10),
        reflect.ValueOf(20),
    }
    results := fn.Call(args)
    
    fmt.Println("Result:", results[0].Int()) // 30
}

Here’s a simple dependency injection container that resolves constructor dependencies:

type Container struct {
    services map[reflect.Type]reflect.Value
}

func NewContainer() *Container {
    return &Container{services: make(map[reflect.Type]reflect.Value)}
}

func (c *Container) Register(constructor interface{}) {
    fn := reflect.ValueOf(constructor)
    if fn.Kind() != reflect.Func {
        panic("must provide a constructor function")
    }
    
    // Assume constructor returns one value
    returnType := fn.Type().Out(0)
    c.services[returnType] = fn
}

func (c *Container) Resolve(target interface{}) {
    targetValue := reflect.ValueOf(target).Elem()
    targetType := targetValue.Type()
    
    constructor, ok := c.services[targetType]
    if !ok {
        panic("service not registered")
    }
    
    // Call constructor and set result
    result := constructor.Call(nil)[0]
    targetValue.Set(result)
}

Practical Applications and Patterns

Reflection shines in building reusable libraries. Here’s a validator that reads struct tags:

func Validate(i interface{}) []error {
    var errors []error
    
    v := reflect.ValueOf(i)
    t := reflect.TypeOf(i)
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        
        tag := field.Tag.Get("validate")
        if tag == "" {
            continue
        }
        
        // Simple required validation
        if tag == "required" {
            if value.IsZero() {
                errors = append(errors, 
                    fmt.Errorf("%s is required", field.Name))
            }
        }
        
        // Email validation (simplified)
        if tag == "email" {
            email := value.String()
            if !strings.Contains(email, "@") {
                errors = append(errors, 
                    fmt.Errorf("%s must be valid email", field.Name))
            }
        }
    }
    
    return errors
}

// Usage
user := User{ID: 1, Email: "invalid"}
if errs := Validate(user); len(errs) > 0 {
    for _, err := range errs {
        fmt.Println(err)
    }
}

Performance Considerations and Best Practices

Reflection is expensive. Here’s a benchmark showing the difference:

func BenchmarkDirect(b *testing.B) {
    u := User{Username: "test"}
    for i := 0; i < b.N; i++ {
        _ = u.Username
    }
}

func BenchmarkReflection(b *testing.B) {
    u := User{Username: "test"}
    v := reflect.ValueOf(u)
    for i := 0; i < b.N; i++ {
        _ = v.FieldByName("Username").String()
    }
}

// Results (approximate):
// BenchmarkDirect-8        1000000000    0.25 ns/op
// BenchmarkReflection-8      10000000     120 ns/op

Reflection is roughly 500x slower for simple field access. Follow these best practices:

Cache reflection results: If you’re repeatedly inspecting the same type, cache the reflect.Type information.

Use code generation: Tools like go generate can produce type-safe code at compile time instead of using reflection.

Prefer type assertions: When you know the possible types, use type switches instead of reflection.

Limit reflection to boundaries: Use reflection at system boundaries (HTTP handlers, database layers) but not in hot paths.

Consider alternatives: Sometimes interface-based designs eliminate the need for reflection entirely.

Reflection is a powerful tool in Go’s arsenal, but it’s not a hammer for every nail. Use it judiciously in libraries and frameworks where the flexibility justifies the cost. For application code, static typing usually provides better performance, safety, and maintainability.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.