Reflection in Go: reflect.Type, reflect.Value, and When Not to Use It

1 min readProgramming

Go's reflect package inspects and manipulates values at runtime when static types aren't known. reflect.TypeOf() returns the type descriptor. reflect.ValueOf() returns a Value that wraps the data. reflect.Value.CanSet() requires the value to be addressable (passed as pointer). Reflection is 10–100× slower than direct calls — use it for serialization frameworks and dependency injection, not hot paths.

goreflection

reflect.Type and reflect.Value

The reflect package exposes two core types:

  • reflect.Type — describes the type (kind, name, fields, methods)
  • reflect.Value — wraps a value with its type, enables read/write at runtime
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age  int
}

func main() {
    u := User{Name: "Alice", Age: 30}

    // reflect.Type: inspect the type
    t := reflect.TypeOf(u)
    fmt.Println(t.Name())       // "User"
    fmt.Println(t.Kind())       // struct
    fmt.Println(t.NumField())   // 2

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("  %s %s\n", field.Name, field.Type) // Name string, Age int
    }

    // reflect.Value: inspect the value
    v := reflect.ValueOf(u)
    fmt.Println(v.Field(0).String()) // "Alice"
    fmt.Println(v.Field(1).Int())    // 30
}

reflect.Kind is the primitive category: struct, ptr, slice, map, func, int, string, etc. reflect.Type.Name() is the named type (e.g., "User"). For anonymous types like []int, Name() returns "".

CanSet and addressability

To modify a value through reflection, it must be addressable — you must pass a pointer:

u := User{Name: "Alice", Age: 30}

// Cannot set — v is a copy, not addressable
v := reflect.ValueOf(u)
fmt.Println(v.Field(0).CanSet()) // false

// Can set — pass pointer, then dereference with Elem()
vp := reflect.ValueOf(&u).Elem()
fmt.Println(vp.Field(0).CanSet()) // true

vp.Field(0).SetString("Bob")
fmt.Println(u.Name) // "Bob"

The rule: reflect.ValueOf(x) where x is not a pointer gives a non-addressable value. Use reflect.ValueOf(&x).Elem() to get an addressable value that mirrors the original variable.

Struct tags

Struct tags are accessible through reflection — this is how encoding/json reads json:"name":

type Config struct {
    Host    string `json:"host" validate:"required"`
    Port    int    `json:"port,omitempty"`
    Timeout int    `json:"timeout" default:"30"`
}

t := reflect.TypeOf(Config{})
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    jsonTag := field.Tag.Get("json")
    validateTag := field.Tag.Get("validate")
    fmt.Printf("%s: json=%q validate=%q\n", field.Name, jsonTag, validateTag)
}
// Host: json="host" validate="required"
// Port: json="port,omitempty" validate=""
// Timeout: json="timeout" validate=""

Calling functions dynamically

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

fn := reflect.ValueOf(Add)
args := []reflect.Value{
    reflect.ValueOf(3),
    reflect.ValueOf(4),
}
results := fn.Call(args)
fmt.Println(results[0].Int()) // 7

Call panics if the argument count or types don't match — reflection bypasses compile-time type checking.

Reflection is 10–100× slower than direct calls — use code generation or type assertions instead for performance-sensitive code

GotchaGo / Performance

Reflection allocates heap memory for reflect.Value wrappers and cannot be inlined by the compiler. A direct struct field access is a single memory load. The same access via reflect.Value.Field(i).Int() involves multiple indirections, interface boxing, and bounds checks. For JSON marshaling at scale, encoding/json uses reflection by default but libraries like jsoniter and sonic generate specialized code at build time or use unsafe pointer arithmetic instead. The standard approach: use reflection in framework initialization (once), cache the results (reflect.Type is comparable), and use cached field offsets for hot paths.

Prerequisites

  • Go interfaces
  • escape analysis
  • CPU caches

Key Points

  • reflect.Value is an interface under the hood — every operation involves interface dispatch.
  • Cache reflect.Type results: reflect.TypeOf(T{}) is cheap but repeated calls in loops waste cycles.
  • unsafe.Pointer + field offsets: used by high-performance serializers to bypass reflection overhead.
  • go generate: code generation (e.g., easyjson, stringer) produces type-specific code at build time, zero reflection at runtime.

Real use cases

Reflection is justified in:

  1. Serialization frameworksencoding/json, encoding/xml, gopkg.in/yaml.v3 read struct tags and iterate fields at runtime because they don't know the struct type at compile time.

  2. Dependency injection — frameworks like google/wire (compile-time) or uber-go/fx (runtime) inspect constructor function signatures to resolve dependencies.

  3. ORM/query builders — map struct fields to database columns, handle arbitrary model types.

  4. Testing utilitiesreflect.DeepEqual for structural comparison, testify/assert for pretty-printing differences.

  5. Plugin systems — loading plugins via plugin.Open() and resolving symbols by name.

// Example: generic deep-clone using reflection
func Clone[T any](v T) T {
    // For simple cases, just return a copy
    // reflect needed when T contains pointers, maps, slices
    src := reflect.ValueOf(v)
    if src.Kind() != reflect.Ptr {
        return v // value types: copy on return
    }
    dst := reflect.New(src.Elem().Type())
    dst.Elem().Set(src.Elem())
    return dst.Interface().(T)
}

You call reflect.ValueOf(myStruct).Field(0).SetString('new'). This panics. Why?

medium

reflect.ValueOf takes any interface{}. CanSet() returns false for non-addressable values.

  • ASetString only works on exported fields
    Incorrect.Unexported fields would cause a panic with a different message ('unexported field'). But even for exported fields, non-addressable values panic with 'reflect.Value.SetString using value obtained using unexported field' — except that's also not the cause here.
  • Breflect.ValueOf(myStruct) creates a copy — the value is non-addressable, so SetString panics with 'reflect: reflect.Value.SetString using value obtained using unexported field'
    Incorrect.Close — but the panic message for non-addressability is 'reflect.Value.SetString using value obtained using unexported field' only for unexported fields. For addressability, it's different.
  • Creflect.ValueOf(myStruct) makes a copy of the struct, and copies are non-addressable. CanSet() returns false, and SetString panics with 'reflect: reflect.Value.SetString using unaddressable value'
    Correct!When you pass a value (not a pointer) to reflect.ValueOf(), Go copies it into an interface{}. Copies are non-addressable — there's no variable to write back to. CanSet() returns false. SetString() checks CanSet() and panics. Fix: pass a pointer and use Elem(): reflect.ValueOf(&myStruct).Elem().Field(0).SetString('new').
  • Dreflect.ValueOf panics when given a struct — it only accepts primitive types
    Incorrect.reflect.ValueOf accepts any value including structs, slices, maps, and pointers. The issue is not the type but the addressability.

Hint:What is the difference between passing a value vs a pointer to reflect.ValueOf? What does CanSet() check?