Go Reflection

Inspect and manipulate types and values at runtime โ€” reflect.Type, reflect.Value, struct tags, and when not to use reflection.

reflect.TypeOf reflect.ValueOf Struct tags reflect.DeepEqual Kind CanSet Interface() Pitfalls
๐Ÿ”

reflect.Type & reflect.Value

  Every Go value has two components in reflection:

  var x float64 = 3.14

  reflect.TypeOf(x)   โ†’  reflect.Type   (describes the type: float64)
  reflect.ValueOf(x)  โ†’  reflect.Value  (holds the value: 3.14)

  Type.Kind()  โ†’  the underlying kind: Bool, Int, Float64, Struct, Slice, โ€ฆ
  Type.Name()  โ†’  the declared type name: "float64", "MyStruct", ""(unnamed)
TypeOf and ValueOf basics Type / Value
import "reflect"

var x float64 = 3.14

t := reflect.TypeOf(x)
fmt.Println(t.Name())   // float64
fmt.Println(t.Kind())   // float64  (same for primitives)

v := reflect.ValueOf(x)
fmt.Println(v.Type())   // float64
fmt.Println(v.Kind())   // float64
fmt.Println(v.Float())  // 3.14  (panics if not float kind)

// Getting the concrete value back out
var result float64 = v.Interface().(float64)
Kind vs Type Kind
type Celsius float64

var c Celsius = 100

t := reflect.TypeOf(c)
t.Name()  // "Celsius"  โ† declared name
t.Kind()  // float64   โ† underlying kind

// Kind is useful for generic operations:
// switch on Kind to handle all int-like, all slice-like, etc.
// Type is useful for exact type identity.
Kind switch โ€” handle any type switch
func describe(i any) string {
    v := reflect.ValueOf(i)
    switch v.Kind() {
    case reflect.Int, reflect.Int64:
        return fmt.Sprintf("int: %d", v.Int())
    case reflect.String:
        return fmt.Sprintf("string: %q", v.String())
    case reflect.Slice:
        return fmt.Sprintf("slice len=%d", v.Len())
    default:
        return v.Type().String()
    }
}
๐Ÿ—๏ธ

Inspecting Structs

Iterating struct fields NumField
type Person struct {
    Name string
    Age  int
    Email string
}

p := Person{"Alice", 30, "alice@example.com"}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)

for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)           // reflect.StructField
    value := v.Field(i)           // reflect.Value
    fmt.Printf("%s = %v\n", field.Name, value.Interface())
}
// Name = Alice
// Age  = 30
// Email = alice@example.com
Field by name FieldByName
t := reflect.TypeOf(p)
f, ok := t.FieldByName("Age")
if ok {
    fmt.Println(f.Name)   // Age
    fmt.Println(f.Type)   // int
    fmt.Println(f.Index)  // [1]
}

v := reflect.ValueOf(p)
fmt.Println(v.FieldByName("Name"))
// Alice
Exported vs unexported fields IsExported
type Config struct {
    Host    string  // exported
    timeout int     // unexported
}

t := reflect.TypeOf(Config{})
for i := 0; i < t.NumField(); i++ {
    f := t.Field(i)
    if f.IsExported() {
        fmt.Println(f.Name, "is exported")
    }
}
// You cannot call .Interface() on unexported fields
// โ€” it panics. Check IsExported first.
๐Ÿท๏ธ

Struct Tags

โ„น๏ธ
Struct tags are raw string literals attached to struct fields. They are invisible at runtime unless accessed via reflection. Standard packages like encoding/json, database/sql, and gopkg.in/yaml.v3 read them to control serialization.
Reading struct tags StructTag.Get
type User struct {
    Name  string `json:"name"             validate:"required,min=2"`
    Email string `json:"email,omitempty"   validate:"required,email"`
    age   int    `json:"-"`               // unexported, json ignores it
}

t := reflect.TypeOf(User{})
f, _ := t.FieldByName("Email")

tag := f.Tag                        // reflect.StructTag
tag.Get("json")                     // "email,omitempty"
tag.Get("validate")                 // "required,email"

// Lookup returns (value, ok) โ€” prefer over Get to distinguish
// missing tag from empty tag value
val, ok := tag.Lookup("json")      // "email,omitempty", true
_, ok  = tag.Lookup("xml")         // "", false
Building a simple struct-to-map encoder Use case
// Encode a struct to map[string]any using json tags as keys
func structToMap(v any) map[string]any {
    out := make(map[string]any)
    rv := reflect.ValueOf(v)
    rt := rv.Type()

    for i := 0; i < rt.NumField(); i++ {
        f := rt.Field(i)
        if !f.IsExported() {
            continue
        }
        key := f.Name
        if tag, ok := f.Tag.Lookup("json"); ok && tag != "-" {
            key = strings.Split(tag, ",")[0]
        }
        out[key] = rv.Field(i).Interface()
    }
    return out
}
โœ๏ธ

Modifying Values

โš ๏ธ
You can only set a value through reflection if it is addressable. To make a value addressable, pass a pointer to reflect.ValueOf and call .Elem() to get the pointed-at value. Calling .Set() on a non-addressable value panics.
Setting a struct field via reflection CanSet ยท Set
type Config struct {
    Timeout int
    Debug   bool
}

cfg := &Config{}

// Must pass pointer and call Elem() to get addressable value
rv := reflect.ValueOf(cfg).Elem()

rv.FieldByName("Timeout").SetInt(30)
rv.FieldByName("Debug").SetBool(true)

fmt.Println(*cfg)  // {30 true}

// Check first to avoid panics on unexported or read-only fields
f := rv.FieldByName("Timeout")
if f.CanSet() {
    f.SetInt(60)
}
New โ€” create a value dynamically reflect.New
// reflect.New(T) allocates a *T and returns reflect.Value
t := reflect.TypeOf(Config{})
ptr := reflect.New(t)   // reflect.Value of *Config

ptr.Elem().FieldByName("Debug").SetBool(true)

// Convert back to concrete type
cfg := ptr.Interface().(*Config)
fmt.Println(cfg.Debug)  // true
Elem() โ€” dereference pointer or interface Elem
x := 42
p := &x

// ValueOf(p) is a reflect.Value of kind Ptr
vp := reflect.ValueOf(p)
fmt.Println(vp.Kind())          // ptr

// .Elem() follows the pointer
ve := vp.Elem()
fmt.Println(ve.Kind())          // int
fmt.Println(ve.CanSet())        // true
ve.SetInt(99)
fmt.Println(x)                   // 99
๐Ÿ“ž

Calling Functions & Methods

Inspecting and calling a function reflect.Value.Call
func Add(a, b int) int { return a + b }

ft := reflect.TypeOf(Add)
fmt.Println(ft.NumIn(), ft.NumOut())  // 2  1
fmt.Println(ft.In(0))               // int
fmt.Println(ft.Out(0))              // int

fv := reflect.ValueOf(Add)
args := []reflect.Value{
    reflect.ValueOf(3),
    reflect.ValueOf(4),
}
results := fv.Call(args)
fmt.Println(results[0].Int())  // 7
Calling a method by name MethodByName
type Greeter struct{ Name string }

func (g Greeter) Hello() string {
    return "Hello, " + g.Name
}

g := Greeter{Name: "Alice"}
v := reflect.ValueOf(g)

m := v.MethodByName("Hello")
if !m.IsValid() {
    fmt.Println("method not found")
    return
}

result := m.Call(nil)
fmt.Println(result[0].String())  // Hello, Alice
โš–๏ธ

reflect.DeepEqual

โ„น๏ธ
reflect.DeepEqual recursively compares two values, including slices, maps, structs, and pointers. It is most useful in tests where == won't work (slices, maps). For production code prefer cmp.Equal from google/go-cmp โ€” it gives better diff output.
DeepEqual examples DeepEqual
// Slices โ€” == doesn't compile, DeepEqual works
a := []int{1, 2, 3}
b := []int{1, 2, 3}
reflect.DeepEqual(a, b)  // true

// Maps
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
reflect.DeepEqual(m1, m2) // true

// nil vs empty slice โ€” NOT equal
reflect.DeepEqual([]int{}, ([]int)(nil))  // false!
DeepEqual gotchas Gotchas
// Struct with unexported fields: DeepEqual CAN compare
// but cmp.Equal panics unless you configure it

// Function values: only equal if both nil
reflect.DeepEqual(fmt.Println, fmt.Println)  // false!

// Pointer comparison: follows pointers and compares
// pointed-at values, not the addresses
x, y := 42, 42
reflect.DeepEqual(&x, &y)  // true (same value)

// NaN != NaN even with DeepEqual
reflect.DeepEqual(math.NaN(), math.NaN())  // false
๐Ÿšง

Pitfalls & When to Avoid Reflection

โš ๏ธ
Reflection bypasses the type system โ€” errors that the compiler would catch become runtime panics. Code using reflection is harder to read, harder to refactor, and significantly slower than direct code. Use it only when generics or interfaces won't work.
Common reflection panics Panics
// 1. Calling a type-specific extractor on the wrong kind
v := reflect.ValueOf("hello")
v.Int()  // panic: call of reflect.Value.Int on string Value

// 2. Setting a non-addressable value
v = reflect.ValueOf(42)   // not addressable (copy)
v.SetInt(99)             // panic: reflect: reflect.Value.SetInt using value obtained using unexported field

// 3. Calling Interface() on unexported struct field
type S struct{ secret int }
reflect.ValueOf(S{1}).Field(0).Interface()  // panic

// 4. Nil pointer dereference
var p *int
reflect.ValueOf(p).Elem()  // panic: reflect: call of reflect.Value.Elem on zero Value
When to use reflection โ€” and when not to Guidelines
// โœ“ USE reflection for:
//   โ€ข Serialization / deserialization libraries (encoding/json, yaml)
//   โ€ข ORM / query builders (field โ†’ column mapping)
//   โ€ข Dependency injection frameworks
//   โ€ข Generic testing utilities (DeepEqual, diff)
//   โ€ข Reading struct tags to drive behaviour

// โœ— AVOID reflection when:
//   โ€ข You could use generics (Go 1.18+) instead
//   โ€ข You could use an interface with a defined method set
//   โ€ข Performance is critical (reflection is ~10-100x slower)
//   โ€ข You control all the types involved

// Rule of thumb: if the caller controls the concrete type,
// use generics. If the library doesn't know the type at
// compile time, use reflection.
Prefer generics for type-safe operations Generics first
// Reflection โ€” works but no compile-time safety
func containsReflect(slice, item any) bool {
    sv := reflect.ValueOf(slice)
    iv := reflect.ValueOf(item)
    for i := 0; i < sv.Len(); i++ {
        if reflect.DeepEqual(sv.Index(i).Interface(), iv.Interface()) {
            return true
        }
    }
    return false
}

// Generics โ€” type-safe, faster, clearer
func contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item { return true }
    }
    return false
}
Performance cost of reflection Performance
// Direct field access: ~1 ns/op
cfg.Timeout = 30

// Reflection field set: ~50-200 ns/op
// (type lookup, boundary checks, allocation)
rv.FieldByName("Timeout").SetInt(30)

// Mitigations:
// Cache reflect.Type and field indices at init time
var timeoutIdx = reflect.TypeOf(Config{}).Field(0).Index[0]

// Use Field(i) with cached index instead of FieldByName
rv.Field(timeoutIdx).SetInt(30)  // ~3x faster
๐Ÿ“‹

Quick Reference

Concept Call Notes
Get typereflect.TypeOf(x)Returns reflect.Type
Get valuereflect.ValueOf(x)Returns reflect.Value
Type namet.Name()Declared name; "" for anonymous types
Underlying kindv.Kind()Bool, Int, Float64, Struct, Sliceโ€ฆ
Extract to anyv.Interface()Panics on unexported field
Number of fieldst.NumField()Struct only
Field by indext.Field(i) / v.Field(i)Returns StructField / Value
Field by namet.FieldByName("F")Returns (StructField, bool)
Exported checkf.IsExported()False for unexported fields
Read struct tagf.Tag.Get("json")Empty string if tag absent
Safe tag readf.Tag.Lookup("json")Returns (val, ok)
Addressable checkv.CanSet()Requires pointer + Elem()
Dereference ptrv.Elem()Works on Ptr and Interface kinds
Set int fieldv.SetInt(n)Panics if not addressable
Set string fieldv.SetString(s)Same addressability requirement
Allocate new Treflect.New(t)Returns *T as reflect.Value
Slice lengthv.Len()Also works for Array, Map, String, Chan
Slice indexv.Index(i)Returns element as reflect.Value
Map keysv.MapKeys()Returns []reflect.Value
Num in/outt.NumIn() / t.NumOut()Function types only
Call functionv.Call([]reflect.Value{โ€ฆ})Returns []reflect.Value
Method by namev.MethodByName("M")Check IsValid() before Call
Deep equalityreflect.DeepEqual(a, b)nil slice โ‰  empty slice
Zero value checkv.IsZero()True if value equals its zero value
Nil checkv.IsNil()Chan, Func, Interface, Map, Ptr, Slice