Reflection in Go

Reflection in Go allows you to inspect and manipulate types at runtime. It’s provided by the reflect package and is useful for building generic libraries, serialization frameworks, and other metaprogramming tasks. However, reflection should be used sparingly as it can make code harder to understand and maintain.

Basic Reflection

The reflect package provides two main types: Type and Value.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x int = 42
    
    // Get the reflect.Value
    v := reflect.ValueOf(x)
    fmt.Println("Value:", v.Int()) // 42
    
    // Get the reflect.Type
    t := reflect.TypeOf(x)
    fmt.Println("Type:", t.String()) // int
    fmt.Println("Kind:", t.Kind())   // int
}

Inspecting Structs

Reflection is particularly useful for working with structs:

package main

import (
    "fmt"
    "reflect"
)

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

func PrintStructInfo(s interface{}) {
    v := reflect.ValueOf(s)
    t := v.Type()
    
    fmt.Printf("Struct: %s\n", t.Name())
    
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        fieldType := t.Field(i)
        
        fmt.Printf("  Field %d: %s %s = %v\n", 
            i, fieldType.Name, fieldType.Type, field.Interface())
        
        // Check for tags
        if tag := fieldType.Tag.Get("json"); tag != "" {
            fmt.Printf("    JSON tag: %s\n", tag)
        }
    }
}

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

Modifying Values

To modify values through reflection, you need to pass a pointer:

package main

import (
    "fmt"
    "reflect"
)

func SetField(obj interface{}, fieldName string, value interface{}) error {
    v := reflect.ValueOf(obj)
    
    // Check if it's a pointer
    if v.Kind() != reflect.Ptr {
        return fmt.Errorf("obj must be a pointer")
    }
    
    // Get the value it points to
    v = v.Elem()
    
    // Find the field
    field := v.FieldByName(fieldName)
    if !field.IsValid() {
        return fmt.Errorf("field %s not found", fieldName)
    }
    
    // Check if field is settable
    if !field.CanSet() {
        return fmt.Errorf("field %s cannot be set", fieldName)
    }
    
    // Set the value
    val := reflect.ValueOf(value)
    if field.Type() != val.Type() {
        return fmt.Errorf("type mismatch: expected %s, got %s", field.Type(), val.Type())
    }
    
    field.Set(val)
    return nil
}

func main() {
    p := &Person{Name: "Bob", Age: 25}
    
    err := SetField(p, "Name", "Charlie")
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    err = SetField(p, "Age", 35)
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    fmt.Printf("%+v\n", p) // &{Name:Charlie Age:35}
}

Calling Functions

You can call functions dynamically using reflection:

package main

import (
    "fmt"
    "reflect"
)

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

func Greet(name string) string {
    return "Hello, " + name
}

func CallFunction(fn interface{}, args ...interface{}) interface{} {
    v := reflect.ValueOf(fn)
    
    // Prepare arguments
    argValues := make([]reflect.Value, len(args))
    for i, arg := range args {
        argValues[i] = reflect.ValueOf(arg)
    }
    
    // Call the function
    resultValues := v.Call(argValues)
    
    // Return the first result (simplified)
    if len(resultValues) > 0 {
        return resultValues[0].Interface()
    }
    
    return nil
}

func main() {
    result1 := CallFunction(Add, 3, 5)
    fmt.Println("Add result:", result1) // 6
    
    result2 := CallFunction(Greet, "World")
    fmt.Println("Greet result:", result2) // Hello, World
}

Creating Types Dynamically

Reflection can create new types at runtime:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    // Create a slice type dynamically
    sliceType := reflect.SliceOf(reflect.TypeOf(""))
    sliceValue := reflect.MakeSlice(sliceType, 0, 0)
    
    // Add elements
    elem1 := reflect.ValueOf("hello")
    sliceValue = reflect.Append(sliceValue, elem1)
    
    elem2 := reflect.ValueOf("world")
    sliceValue = reflect.Append(sliceValue, elem2)
    
    fmt.Println(sliceValue.Interface()) // [hello world]
    
    // Create a map type
    mapType := reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf(0))
    mapValue := reflect.MakeMap(mapType)
    
    key1 := reflect.ValueOf("key1")
    value1 := reflect.ValueOf(42)
    mapValue.SetMapIndex(key1, value1)
    
    fmt.Println(mapValue.Interface()) // map[key1:42]
}

JSON Serialization with Reflection

A simple JSON encoder using reflection:

package main

import (
    "fmt"
    "reflect"
    "strconv"
    "strings"
)

func MarshalJSON(v interface{}) string {
    val := reflect.ValueOf(v)
    typ := val.Type()
    
    if typ.Kind() == reflect.Struct {
        var fields []string
        for i := 0; i < val.NumField(); i++ {
            field := val.Field(i)
            fieldType := typ.Field(i)
            
            key := fieldType.Name
            var value string
            
            switch field.Kind() {
            case reflect.String:
                value = fmt.Sprintf("\"%s\"", field.String())
            case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                value = strconv.FormatInt(field.Int(), 10)
            case reflect.Bool:
                value = strconv.FormatBool(field.Bool())
            default:
                value = "\"unknown\""
            }
            
            fields = append(fields, fmt.Sprintf("\"%s\":%s", key, value))
        }
        return "{" + strings.Join(fields, ",") + "}"
    }
    
    return "{}"
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    json := MarshalJSON(p)
    fmt.Println(json) // {"Name":"Alice","Age":30}
}

When to Use Reflection

Reflection is powerful but should be used carefully:

Good uses:

  • Building serialization libraries (JSON, XML)
  • Dependency injection frameworks
  • ORM libraries
  • Testing utilities

Avoid when:

  • You can solve the problem with interfaces
  • Performance is critical
  • Code clarity is important

Performance Considerations

Reflection is slower than direct code. Here’s a comparison:

package main

import (
    "reflect"
    "testing"
)

func DirectAccess(p Person) string {
    return p.Name
}

func ReflectAccess(p Person) string {
    v := reflect.ValueOf(p)
    field := v.FieldByName("Name")
    return field.String()
}

func BenchmarkDirect(b *testing.B) {
    p := Person{Name: "Test"}
    for i := 0; i < b.N; i++ {
        _ = DirectAccess(p)
    }
}

func BenchmarkReflect(b *testing.B) {
    p := Person{Name: "Test"}
    for i := 0; i < b.N; i++ {
        _ = ReflectAccess(p)
    }
}

Direct access is much faster than reflection.

Best Practices

  1. Use reflection sparingly: Only when necessary
  2. Check for nil values: Always validate before using
  3. Handle errors gracefully: Reflection operations can panic
  4. Cache reflection results: If you need to do the same operations repeatedly
  5. Document reflective code: Explain what you’re doing and why

Reflection is a powerful tool in Go’s toolbox, but like any powerful tool, it should be used responsibly. When possible, prefer compile-time solutions over runtime reflection.

For more details, see the reflect package documentation.

If you need to work with JSON in Go, check out our JSON handling tutorial.

Last updated on