9.1 Reflection

Reflection in Go allows programs to examine and manipulate their own structure at runtime. It's a powerful but complex feature that should be used judiciously.

Reflection Basics

What is reflection?

Reflection is the ability of a program to examine its own structure, particularly types and values, at runtime. In Go, reflection is provided by the reflect package.

Go is statically typed—types are known at compile time. However, sometimes you need to work with types that aren't known until runtime. Reflection enables serialization and deserialization (converting data to/from JSON, XML, etc.), generic operations on values of unknown types, framework code that needs to work with arbitrary types, and testing utilities that work with any type.

Reflection in Go is based on two main types: reflect.Type, which represents a Go type, and reflect.Value, which represents a Go value.

Basic example

package main

import (
    "fmt"
    "reflect"
)

func main() {
    x := 42
    v := reflect.ValueOf(x)
    t := reflect.TypeOf(x)

    fmt.Println("Type:", t)           // int
    fmt.Println("Kind:", t.Kind())    // int
    fmt.Println("Value:", v.Int())    // 42
}

Reflection is powerful because it enables generic operations on unknown types, but it's also dangerous because it bypasses type safety, can cause panics, and is slower than direct operations. Use reflection sparingly—only when necessary, as it makes code harder to understand and maintain.

Type and Value

Understanding reflect.Type and reflect.Value is fundamental to using reflection.

reflect.Type

reflect.Type represents a Go type. You can get it using reflect.TypeOf().

Getting the type of a value:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x int = 42
    var s string = "hello"
    var f float64 = 3.14

    tx := reflect.TypeOf(x)
    ts := reflect.TypeOf(s)
    tf := reflect.TypeOf(f)

    fmt.Println("Type of x:", tx)  // int
    fmt.Println("Type of s:", ts)  // string
    fmt.Println("Type of f:", tf)  // float64
}

Type information

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 name:", t.Name())        // Person
    fmt.Println("Kind:", t.Kind())             // struct
    fmt.Println("Number of fields:", t.NumField())  // 2

    // Iterate over fields
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field %d: %s (type: %s)\n", 
            i, field.Name, field.Type)
    }
}

reflect.Value

reflect.Value represents a Go value. You can get it using reflect.ValueOf().

Getting the value:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    x := 42
    v := reflect.ValueOf(x)

    fmt.Println("Value:", v)
    fmt.Println("Type:", v.Type())
    fmt.Println("Kind:", v.Kind())
    fmt.Println("Can set:", v.CanSet())  // false (value is not settable)
}

When you call reflect.ValueOf() on a value (not a pointer), you get a copy. To modify the original, you need a pointer:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    x := 42

    // Not settable (value, not pointer)
    v1 := reflect.ValueOf(x)
    fmt.Println("Can set v1:", v1.CanSet())  // false

    // Settable (pointer)
    v2 := reflect.ValueOf(&x)
    fmt.Println("Can set v2:", v2.CanSet())  // false (pointer itself)

    // Need to get element of pointer
    v3 := v2.Elem()
    fmt.Println("Can set v3:", v3.CanSet())  // true

    // Now we can set
    v3.SetInt(100)
    fmt.Println("x after setting:", x)  // 100
}

Modifying values:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    x := 42
    v := reflect.ValueOf(&x).Elem()  // Get settable value

    if v.CanSet() {
        v.SetInt(100)
        fmt.Println("x:", x)  // 100
    }
}

Common Use Cases

Reflection is commonly used in several scenarios.

1. JSON serialization/deserialization

The encoding/json package uses reflection to marshal and unmarshal data:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func inspectStruct(v interface{}) {
    t := reflect.TypeOf(v)
    if t.Kind() != reflect.Struct {
        return
    }

    fmt.Printf("Struct: %s\n", t.Name())
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        jsonTag := field.Tag.Get("json")
        fmt.Printf("  Field: %s, JSON tag: %s\n", field.Name, jsonTag)
    }
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    inspectStruct(p)

    // JSON uses reflection internally
    data, _ := json.Marshal(p)
    fmt.Println("JSON:", string(data))
}

2. Type checking and conversion

package main

import (
    "fmt"
    "reflect"
)

func processValue(v interface{}) {
    val := reflect.ValueOf(v)

    switch val.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        fmt.Printf("Integer: %d\n", val.Int())
    case reflect.String:
        fmt.Printf("String: %s\n", val.String())
    case reflect.Bool:
        fmt.Printf("Bool: %t\n", val.Bool())
    default:
        fmt.Printf("Other type: %s\n", val.Type())
    }
}

func main() {
    processValue(42)
    processValue("hello")
    processValue(true)
    processValue(3.14)
}

3. Calling functions dynamically

package main

import (
    "fmt"
    "reflect"
)

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

func Multiply(a, b int) int {
    return a * b
}

func callFunction(fn interface{}, args ...interface{}) []reflect.Value {
    v := reflect.ValueOf(fn)
    if v.Kind() != reflect.Func {
        panic("not a function")
    }

    // Convert args to reflect.Values
    in := make([]reflect.Value, len(args))
    for i, arg := range args {
        in[i] = reflect.ValueOf(arg)
    }

    // Call function
    return v.Call(in)
}

func main() {
    result := callFunction(Add, 10, 20)
    fmt.Println("Add(10, 20) =", result[0].Int())  // 30

    result = callFunction(Multiply, 5, 6)
    fmt.Println("Multiply(5, 6) =", result[0].Int())  // 30
}

4. Creating values dynamically

package main

import (
    "fmt"
    "reflect"
)

func createSlice(t reflect.Type, length, capacity int) reflect.Value {
    sliceType := reflect.SliceOf(t)
    return reflect.MakeSlice(sliceType, length, capacity)
}

func main() {
    // Create []int with length 5, capacity 10
    intSliceType := reflect.TypeOf(0)  // int type
    slice := createSlice(intSliceType, 5, 10)

    // Convert back to []int
    intSlice := slice.Interface().([]int)
    fmt.Println("Slice:", intSlice)  // [0 0 0 0 0]
    fmt.Println("Length:", slice.Len())
    fmt.Println("Capacity:", slice.Cap())
}

5. Struct field access

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func getFieldValue(v interface{}, fieldName string) (interface{}, bool) {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }

    if val.Kind() != reflect.Struct {
        return nil, false
    }

    field := val.FieldByName(fieldName)
    if !field.IsValid() {
        return nil, false
    }

    return field.Interface(), true
}

func main() {
    p := Person{Name: "Alice", Age: 30}

    name, ok := getFieldValue(p, "Name")
    if ok {
        fmt.Println("Name:", name)  // Alice
    }

    age, ok := getFieldValue(&p, "Age")
    if ok {
        fmt.Println("Age:", age)  // 30
    }
}

When to Use Reflection

When reflection is appropriate

  1. Serialization libraries: JSON, XML, YAML encoders/decoders
  2. ORM frameworks: Database mapping tools
  3. Validation libraries: Runtime validation of struct fields
  4. Configuration loading: Reading config into structs
  5. Testing utilities: Generic test helpers

When NOT to use reflection

  1. Performance-critical code: Reflection is slower than direct operations
  2. Simple type operations: Use generics (Go 1.18+) or interfaces instead
  3. When types are known: Use direct type operations
  4. When clarity matters: Reflection makes code harder to understand

Example: When NOT to use reflection

// ❌ BAD: Using reflection unnecessarily
func addReflection(a, b interface{}) interface{} {
    va := reflect.ValueOf(a)
    vb := reflect.ValueOf(b)

    if va.Kind() == reflect.Int && vb.Kind() == reflect.Int {
        return va.Int() + vb.Int()
    }
    // ... more reflection code
}

// ✅ GOOD: Use generics instead (Go 1.18+)
func add[T int | float64](a, b T) T {
    return a + b
}

// Or use interfaces for runtime polymorphism
type Adder interface {
    Add(other Adder) Adder
}

Performance Considerations

Reflection has significant performance overhead compared to direct operations.

Reflection has significant performance overhead because types must be checked at runtime, there are extra layers of indirection, the compiler can't optimize reflected code, and reflection often allocates memory.

Performance comparison

package main

import (
    "reflect"
    "testing"
)

type Person struct {
    Name string
    Age  int
}

// Direct access
func BenchmarkDirect(b *testing.B) {
    p := Person{Name: "Alice", Age: 30}
    for i := 0; i < b.N; i++ {
        _ = p.Name
        _ = p.Age
    }
}

// Reflection access
func BenchmarkReflection(b *testing.B) {
    p := Person{Name: "Alice", Age: 30}
    v := reflect.ValueOf(p)
    for i := 0; i < b.N; i++ {
        _ = v.FieldByName("Name").String()
        _ = v.FieldByName("Age").Int()
    }
}

Typical performance impact

To minimize reflection overhead, cache reflect.Type and reflect.Value instead of recreating them in loops, use type assertions when possible (they're faster than reflection), limit reflection scope to only where necessary, and consider code generation to create type-specific code instead.

Example: Caching reflection values

// ❌ BAD: Recreating reflection values
func slowProcess(v interface{}) {
    for i := 0; i < 1000; i++ {
        t := reflect.TypeOf(v)  // Recreated every iteration
        val := reflect.ValueOf(v)
        // ... use t and val
    }
}

// ✅ GOOD: Cache reflection values
var cachedType = reflect.TypeOf((*MyType)(nil)).Elem()
var cachedValue reflect.Value

func fastProcess(v interface{}) {
    // Cache once
    if !cachedValue.IsValid() {
        cachedValue = reflect.New(cachedType).Elem()
    }
    // Reuse cached values
}

Best practices

  1. Measure first: Don't assume reflection is the bottleneck
  2. Use sparingly: Only when necessary
  3. Cache when possible: Reuse reflection values
  4. Consider alternatives: Generics, interfaces, code generation
  5. Document why: Explain why reflection is needed

Reflection is a powerful tool, but it should be used judiciously. When possible, prefer type-safe alternatives like generics or interfaces. Use reflection when you truly need to work with unknown types at runtime, such as in serialization libraries or framework code.