Reflect Internals and Cost

Updated

September 8, 2026

Reflect Internals and Cost

Overview

Package reflect exposes runtime type metadata (rtype) and dynamic values (Value). It powers encoding/json, dependency injection frameworks, and ORMs — and it is a common production hotspot when used on every request without caching.

Usage intro: Reflection.

Diagram: Reflect path

flow:
  [V]
       |
       v
  [Flags]

Core Types

t := reflect.TypeOf(x)   // runtime type descriptor (nil-safe only if x typed)
v := reflect.ValueOf(x)  // dynamic value
API Role
Type Size, kind, fields, methods — often cacheable
Value Read/write/call — easy to allocate
Kind Int, Struct, Slice, …
Interface Box back to any

What Metadata Lives Where

interface / concrete
       |
       v
    rtype  (uncommonType, struct fields, method table…)
       |
Value flags: addressable? settability? indir?

ValueOf on a non-pointer often yields a non-addressable copy — Set fails. Pass pointers for mutation:

rv := reflect.ValueOf(&x).Elem()
rv.SetInt(3)

Allocations and Interface Boxing

Common alloc sources:

  1. Value.Interface() boxing
  2. ValueOf of small values that escape
  3. Building []reflect.Value for Call
  4. Pathological use inside tight loops without type caches
// Cache field indexes by type
var cache sync.Map // reflect.Type -> []int indices

encoding/json caches struct field info per type — copy that idea.

Method Calls via Reflect

m := reflect.ValueOf(obj).MethodByName("Do")
results := m.Call(nil)

Much slower than a direct call or interface method. Prefer interfaces for hot polymorphic paths; use reflect for glue and frameworks.

Unsafe Adjacent

reflect.Type and unsafe.Pointer interactions exist for advanced serializers. Prefer well-tested libraries over hand-rolled unsafe reflection unless you accept maintenance cost.

See unsafe.

When Reflect Is Justified

Use Verdict
One-time config decode Fine
Per-request JSON (stdlib) Fine — heavily optimized
Per-request generic mapper over large structs Cache or codegen (go generate, json struct tags already)
Game loop / packet parse Avoid; use codegen or concrete code

Experiment

go mod init example
go test -bench=. -benchmem
package main

import (
    "reflect"
    "testing"
)

type S struct {
    A int
    B string
}

func direct(s *S) int { return s.A }

func viaReflect(s *S) int {
    return int(reflect.ValueOf(s).Elem().FieldByName("A").Int())
}

func BenchmarkDirect(b *testing.B) {
    s := &S{A: 42}
    for i := 0; i < b.N; i++ {
        _ = direct(s)
    }
}

func BenchmarkReflect(b *testing.B) {
    s := &S{A: 42}
    for i := 0; i < b.N; i++ {
        _ = viaReflect(s)
    }
}

// Keep a main so go run works if needed
func main() {}

Put benchmarks in main_test.go or rename package for pure test module:

# simpler: single file package reflectbench_test

Alternatively run:

// file: cost_test.go
package cost

import (
    "reflect"
    "testing"
)

type S struct{ A int }

func BenchmarkFieldDirect(b *testing.B) {
    s := S{A: 1}
    for i := 0; i < b.N; i++ {
        _ = s.A
    }
}

func BenchmarkFieldReflect(b *testing.B) {
    s := S{A: 1}
    for i := 0; i < b.N; i++ {
        _ = reflect.ValueOf(s).Field(0).Int()
    }
}
mkdir /tmp/reflect-cost && cd /tmp/reflect-cost
go mod init example
# write cost_test.go as above
go test -bench=. -benchmem

What to notice: Reflect field access is often orders of magnitude slower and may allocate depending on path.

Try next: Cache reflect.Type and field index; re-benchmark FieldByName vs Field(i).