Skip to content

Reflection Advanced

Introduction

Reflection in Go allows a program to inspect and manipulate its own types and values at runtime. The reflect package provides this capability, enabling you to examine struct fields, read struct tags, call functions dynamically, and build types on the fly.

Reflection is the engine behind Go's standard library: encoding/json, encoding/xml, fmt.Println, database/sql, and text/template all rely heavily on it. Understanding reflection is essential for writing libraries and frameworks — but equally important is knowing when not to use it.

Syntax & Usage

reflect.Type and reflect.Value

Every Go value has two components that reflection exposes:

  • reflect.Type — the type of the value (struct, int, func, etc.)
  • reflect.Value — the value itself, wrapped in a reflection container
import "reflect"

x := 42
s := "hello"
u := User{Name: "Alice", Age: 30}

// reflect.TypeOf returns the reflect.Type
fmt.Println(reflect.TypeOf(x))   // int
fmt.Println(reflect.TypeOf(s))   // string
fmt.Println(reflect.TypeOf(u))   // main.User

// reflect.ValueOf returns the reflect.Value
fmt.Println(reflect.ValueOf(x))  // 42
fmt.Println(reflect.ValueOf(s))  // hello
fmt.Println(reflect.ValueOf(u))  // {Alice 30}

Kind vs Type

Type is the specific Go type (main.User, int, []string).
Kind is the category of type (struct, int, slice).

type UserID int64

var id UserID = 42

t := reflect.TypeOf(id)
fmt.Println(t)        // main.UserID (the Type)
fmt.Println(t.Kind()) // int64 (the Kind — the underlying category)
fmt.Println(t.Name()) // UserID

// Kind is one of the constants in reflect package:
// Bool, Int, Int8, ..., Float32, Float64,
// String, Array, Slice, Map, Chan, Func,
// Ptr, Struct, Interface, UnsafePointer

Inspecting Struct Fields and Tags

type User struct {
    Name     string    `json:"name" validate:"required"`
    Email    string    `json:"email" validate:"email"`
    Age      int       `json:"age,omitempty" validate:"gte=0,lte=150"`
    IsAdmin  bool      `json:"-"` // Excluded from JSON
    created  time.Time // Unexported — not accessible via reflection from other packages
}

func inspectStruct(v interface{}) {
    t := reflect.TypeOf(v)

    // If pointer, dereference to get the struct type
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }

    if t.Kind() != reflect.Struct {
        fmt.Println("not a struct")
        return
    }

    fmt.Printf("Struct: %s (%d fields)\n", t.Name(), t.NumField())

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("  Field: %-10s Type: %-12s", field.Name, field.Type)

        // Read struct tags
        if jsonTag, ok := field.Tag.Lookup("json"); ok {
            fmt.Printf(" json:%q", jsonTag)
        }
        if validateTag, ok := field.Tag.Lookup("validate"); ok {
            fmt.Printf(" validate:%q", validateTag)
        }

        fmt.Printf(" Exported: %v\n", field.IsExported())
    }
}

// Output:
// Struct: User (5 fields)
//   Field: Name       Type: string       json:"name" validate:"required" Exported: true
//   Field: Email      Type: string       json:"email" validate:"email" Exported: true
//   Field: Age        Type: int          json:"age,omitempty" validate:"gte=0,lte=150" Exported: true
//   Field: IsAdmin    Type: bool         json:"-" Exported: true
//   Field: created    Type: time.Time    Exported: false

Setting Values (Must Be Addressable)

// reflect.Value can set values, but only if the value is ADDRESSABLE.
// This requires passing a pointer.

x := 42
v := reflect.ValueOf(x)
// v.SetInt(100) // PANICS — v is not addressable (it's a copy)

// Pass a pointer, then call Elem() to get the addressable value
v = reflect.ValueOf(&x).Elem()
v.SetInt(100)
fmt.Println(x) // 100

// Setting struct fields
type Config struct {
    Host string
    Port int
}

cfg := Config{Host: "localhost", Port: 8080}
v = reflect.ValueOf(&cfg).Elem()

hostField := v.FieldByName("Host")
if hostField.IsValid() && hostField.CanSet() {
    hostField.SetString("0.0.0.0")
}

portField := v.FieldByName("Port")
if portField.IsValid() && portField.CanSet() {
    portField.SetInt(9090)
}

fmt.Println(cfg) // {0.0.0.0 9090}

Calling Functions Dynamically

func add(a, b int) int { return a + b }
func greet(name string) string { return "Hello, " + name }

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

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

    // Call the function
    results := v.Call(in)

    // Convert results back to interface{}
    out := make([]interface{}, len(results))
    for i, r := range results {
        out[i] = r.Interface()
    }
    return out
}

result := callDynamic(add, 3, 4)
fmt.Println(result[0]) // 7

result = callDynamic(greet, "World")
fmt.Println(result[0]) // Hello, World

The Three Laws of Reflection

Rob Pike's three laws define the relationship between Go values and reflection:

// Law 1: Reflection goes from interface value to reflection object.
// reflect.TypeOf and reflect.ValueOf accept interface{} and return
// reflect.Type / reflect.Value.
var x float64 = 3.14
v := reflect.ValueOf(x) // Go value → reflect.Value

// Law 2: Reflection goes from reflection object to interface value.
// reflect.Value.Interface() returns the value as interface{}.
y := v.Interface().(float64) // reflect.Value → Go value
fmt.Println(y)               // 3.14

// Law 3: To modify a reflection object, the value must be settable.
// Settable means the reflect.Value holds the ADDRESS of the original variable.
v = reflect.ValueOf(&x).Elem() // Must pass pointer, then Elem()
v.SetFloat(2.71)
fmt.Println(x) // 2.71

Practical Example: Struct Tag Parser (Validation Library)

type ValidationError struct {
    Field   string
    Tag     string
    Message string
}

func Validate(v interface{}) []ValidationError {
    var errors []ValidationError

    val := reflect.ValueOf(v)
    typ := reflect.TypeOf(v)

    if val.Kind() == reflect.Ptr {
        val = val.Elem()
        typ = typ.Elem()
    }

    if val.Kind() != reflect.Struct {
        return errors
    }

    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        value := val.Field(i)
        tag := field.Tag.Get("validate")

        if tag == "" || !field.IsExported() {
            continue
        }

        rules := strings.Split(tag, ",")
        for _, rule := range rules {
            if err := applyRule(field.Name, value, rule); err != nil {
                errors = append(errors, *err)
            }
        }
    }

    return errors
}

func applyRule(fieldName string, value reflect.Value, rule string) *ValidationError {
    switch {
    case rule == "required":
        if value.IsZero() {
            return &ValidationError{
                Field:   fieldName,
                Tag:     "required",
                Message: fmt.Sprintf("%s is required", fieldName),
            }
        }

    case strings.HasPrefix(rule, "min="):
        minStr := strings.TrimPrefix(rule, "min=")
        min, _ := strconv.Atoi(minStr)
        switch value.Kind() {
        case reflect.String:
            if value.Len() < min {
                return &ValidationError{
                    Field:   fieldName,
                    Tag:     "min",
                    Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min),
                }
            }
        case reflect.Int, reflect.Int64:
            if value.Int() < int64(min) {
                return &ValidationError{
                    Field:   fieldName,
                    Tag:     "min",
                    Message: fmt.Sprintf("%s must be at least %d", fieldName, min),
                }
            }
        }

    case strings.HasPrefix(rule, "max="):
        maxStr := strings.TrimPrefix(rule, "max=")
        max, _ := strconv.Atoi(maxStr)
        switch value.Kind() {
        case reflect.String:
            if value.Len() > max {
                return &ValidationError{
                    Field:   fieldName,
                    Tag:     "max",
                    Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max),
                }
            }
        case reflect.Int, reflect.Int64:
            if value.Int() > int64(max) {
                return &ValidationError{
                    Field:   fieldName,
                    Tag:     "max",
                    Message: fmt.Sprintf("%s must be at most %d", fieldName, max),
                }
            }
        }
    }
    return nil
}

// Usage
type CreateUserRequest struct {
    Name  string `validate:"required,min=2,max=50"`
    Email string `validate:"required"`
    Age   int    `validate:"min=0,max=150"`
}

req := CreateUserRequest{Name: "A", Age: 200}
errs := Validate(req)
for _, e := range errs {
    fmt.Printf("%s: %s\n", e.Field, e.Message)
}
// Name: Name must be at least 2 characters
// Email: Email is required
// Age: Age must be at most 150

Practical Example: Generic JSON-to-Map Converter

func StructToMap(v interface{}) map[string]interface{} {
    result := make(map[string]interface{})

    val := reflect.ValueOf(v)
    typ := reflect.TypeOf(v)

    if val.Kind() == reflect.Ptr {
        val = val.Elem()
        typ = typ.Elem()
    }

    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        if !field.IsExported() {
            continue
        }

        // Use json tag name if available
        name := field.Name
        if jsonTag := field.Tag.Get("json"); jsonTag != "" {
            parts := strings.Split(jsonTag, ",")
            if parts[0] == "-" {
                continue
            }
            if parts[0] != "" {
                name = parts[0]
            }
            // Handle omitempty
            if len(parts) > 1 && parts[1] == "omitempty" && val.Field(i).IsZero() {
                continue
            }
        }

        result[name] = val.Field(i).Interface()
    }

    return result
}

Inspecting Interface Implementations at Runtime

var errorType = reflect.TypeOf((*error)(nil)).Elem()
var stringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()

func implementsError(v interface{}) bool {
    return reflect.TypeOf(v).Implements(errorType)
}

func implementsStringer(v interface{}) bool {
    t := reflect.TypeOf(v)
    return t.Implements(stringerType) || reflect.PtrTo(t).Implements(stringerType)
}

Quick Reference

Operation Function/Method Notes
Get type reflect.TypeOf(v) Returns reflect.Type
Get value reflect.ValueOf(v) Returns reflect.Value
Type category t.Kind() reflect.Struct, reflect.Int, etc.
Type name t.Name() "User", "" for unnamed types
Struct fields t.NumField(), t.Field(i) Returns reflect.StructField
Field by name t.FieldByName("Name") Also available on reflect.Value
Struct tag field.Tag.Get("json") Returns tag value string
Set value v.SetInt(42), v.SetString("") Must be addressable (use & + Elem())
Settable? v.CanSet() Check before calling Set*
Interface v.Interface() Convert reflect.Valueinterface{}
Pointer elem v.Elem() Dereference pointer or interface
Call function v.Call(args) args is []reflect.Value
Create pointer reflect.New(t) Like new(T) — returns *T as reflect.Value
Make slice reflect.MakeSlice(t, len, cap) Dynamic slice creation
Make map reflect.MakeMap(t) Dynamic map creation

Best Practices

  1. Exhaust all alternatives before using reflection — generics (1.18+), interfaces, and code generation are almost always better.

  2. Cache reflect.Typereflect.TypeOf itself is cheap, but repeatedly computing types in hot loops adds up. Store types in package-level variables.

    var userType = reflect.TypeOf(User{})
    
  3. Check CanSet() before setting — attempting to set a non-settable value panics.

  4. Handle pointers at entry points — always check Kind() == reflect.Ptr and call Elem() at the start of reflection functions.

  5. Use field.IsExported() — never attempt to get or set unexported fields from outside the package (it panics).

  6. Write comprehensive tests — reflection code is hard to reason about statically. Test with diverse types including edge cases (nil, zero values, nested structs).

Common Pitfalls

Panic on Non-Settable Values

x := 42
v := reflect.ValueOf(x)
v.SetInt(100) // PANIC: reflect.Value.SetInt using unaddressable value

// ✅ Pass pointer and use Elem()
v = reflect.ValueOf(&x).Elem()
v.SetInt(100) // Works

Panic on Wrong Kind Operations

v := reflect.ValueOf("hello")
v.SetInt(42) // PANIC: reflect.Value.SetInt on string Value

v = reflect.ValueOf(42)
v.FieldByName("X") // PANIC: not a struct

reflect.DeepEqual Gotchas

// nil slice vs empty slice
var s1 []int          // nil
s2 := []int{}         // empty, non-nil
reflect.DeepEqual(s1, s2) // false! Different from most expectations

// Time comparison ignores wall clock
t1 := time.Now()
t2 := t1.Round(0) // Strips monotonic clock reading
reflect.DeepEqual(t1, t2) // false! Use t1.Equal(t2) instead

// Unexported fields are compared
// This means two structs with different unexported fields are not equal,
// even if all exported fields match

Interface vs Concrete Type Confusion

var err error = fmt.Errorf("oops")

// TypeOf sees the INTERFACE, not the concrete type
// Actually, TypeOf sees through the interface to the concrete type
fmt.Println(reflect.TypeOf(err))        // *errors.errorString
fmt.Println(reflect.TypeOf(err).Kind()) // ptr

// But if you want to check the interface type itself:
// You need the pointer-to-interface trick
var errType = reflect.TypeOf((*error)(nil)).Elem()
fmt.Println(errType) // error

Performance Considerations

Reflection is 10–100x slower than direct code for equivalent operations:

// Benchmark: direct field access vs reflection
func BenchmarkDirect(b *testing.B) {
    u := User{Name: "Alice"}
    for i := 0; i < b.N; i++ {
        _ = u.Name // ~0.3 ns
    }
}

func BenchmarkReflection(b *testing.B) {
    u := User{Name: "Alice"}
    v := reflect.ValueOf(u)
    for i := 0; i < b.N; i++ {
        _ = v.FieldByName("Name").String() // ~50-100 ns
    }
}

func BenchmarkReflectionCached(b *testing.B) {
    u := User{Name: "Alice"}
    v := reflect.ValueOf(u)
    t := v.Type()
    nameIdx, _ := t.FieldByName("Name")
    for i := 0; i < b.N; i++ {
        _ = v.FieldByIndex(nameIdx.Index).String() // ~20-30 ns (faster with cached index)
    }
}

Strategies to reduce reflection cost:

  1. Cache field indices and types — compute them once at init time, not per-call.

  2. Use code generationgo generate with tools like easyjson, msgp, or stringer produces non-reflective code.

  3. Use generics (Go 1.18+) — many patterns that previously required reflection (type-safe containers, serialization helpers) can now use generics.

  4. Limit reflection to startup/init — parse struct tags and build lookup tables at init time, use the tables at runtime.

// Example: cache struct metadata at init time
type fieldInfo struct {
    index    int
    jsonName string
    required bool
}

var typeCache sync.Map // map[reflect.Type][]fieldInfo

func getFieldInfo(t reflect.Type) []fieldInfo {
    if cached, ok := typeCache.Load(t); ok {
        return cached.([]fieldInfo)
    }
    // Compute and cache
    var fields []fieldInfo
    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        if !f.IsExported() {
            continue
        }
        info := fieldInfo{index: i}
        if tag := f.Tag.Get("json"); tag != "" {
            parts := strings.Split(tag, ",")
            info.jsonName = parts[0]
        }
        if tag := f.Tag.Get("validate"); strings.Contains(tag, "required") {
            info.required = true
        }
        fields = append(fields, info)
    }
    typeCache.Store(t, fields)
    return fields
}

Generics vs Reflection (When to Use Which)

Scenario Use Generics Use Reflection
Type-safe container (Stack, Set)
Functional utilities (Map, Filter)
JSON serialization/deserialization ✅ (struct tags)
ORM field mapping ✅ (struct tags)
Validation library ✅ (struct tags)
fmt.Println-like formatting ✅ (arbitrary types)
Testing deep equality ✅ (reflect.DeepEqual)
Plugin/middleware systems ✅ (dynamic dispatch)

Interview Tips

Interview Tip

When asked about reflection, emphasize the trade-offs: it sacrifices compile-time type safety and performance for runtime flexibility. A good answer is: "I use reflection for libraries that must handle arbitrary user-defined types — like JSON serialization or validation — but never in application-level code where generics or interfaces suffice."

Interview Tip

The three laws of reflection (interface → reflection object, reflection object → interface, settability requires addressability) are frequently asked in interviews. Know them and be able to explain the third law with a pointer example.

Interview Tip

If asked "How does encoding/json work?", explain: it uses reflect.TypeOf to inspect struct fields, reads json struct tags for field naming, and uses reflect.ValueOf to get/set field values. Mention that this is why JSON marshaling is ~10x slower than code-generated alternatives like easyjson.

Interview Tip

A strong answer includes knowing when NOT to use reflection: "Since Go 1.18, generics handle most of the type-safe abstraction use cases that previously required reflection. I'd only reach for reflection when I need to inspect struct tags, handle truly unknown types at runtime, or build framework-level utilities."

Key Takeaways

  • Reflection enables runtime type inspection and manipulation via reflect.Type and reflect.Value.
  • Kind is the type category (struct, int, slice); Type is the specific Go type (User, int64).
  • Values must be addressable (passed by pointer) to be set via reflection.
  • Reflection is 10–100x slower than direct code — cache metadata, limit to init paths.
  • Generics replace most reflection use cases since Go 1.18 — reflection remains necessary for struct tags, arbitrary type handling, and framework internals.
  • The encoding/json, database/sql, and fmt packages are built on reflection — understanding it helps you understand the standard library.