AlgoMaster Logo

Reflection Performance

Low Priority15 min readUpdated June 6, 2026

Reflection trades raw speed for flexibility. Every operation the compiler would normally inline becomes a runtime dance through interface boxing, type lookups, and small allocations. This chapter covers why reflection is slow, real benchmark numbers comparing direct access to reflect.Value access, caching strategies that recover most of the cost, and the alternatives (code generation, generics, plain interfaces) that avoid the cost entirely.

Why Reflection Is Slow

A direct field read like product.Price is essentially free. The compiler knows the type, the field's offset inside the struct, and the size of the value. It emits a single load instruction with a constant offset. Modern CPUs run it in roughly one nanosecond.

The reflective version of the same read does more work than most people realize:

Each step costs something. Boxing the value into interface{} writes two pointers. reflect.ValueOf reads those pointers and constructs a reflect.Value struct. FieldByName walks the struct's field table comparing strings. Calling .Interface() on the result allocates if the value is a non-pointer type that doesn't fit in a word. None of this can be inlined, because the compiler doesn't know which type or field the program will ask for at run time.

The big four cost sources, in rough order of impact:

  1. Boxing into `interface{}`. Every reflective entry point takes an interface{} argument. Calling reflect.ValueOf(x) first puts x into an interface value, which means writing the type pointer and the data pointer somewhere. For non-pointer types, the data often has to escape to the heap.
  2. Type metadata lookups. FieldByName("Price") searches the struct's field list by string. For a struct with 10 fields, that's up to 10 string comparisons. The runtime caches some of this, but the cache is per-type, not per-field.
  3. Lack of inlining. The compiler can inline a direct field read across a function boundary. It cannot inline v.FieldByName("Price") because the field name only exists as data, not code.
  4. Allocations on `Interface()`. Turning a reflect.Value back into a usable value boxes again. For small types like int or float64, this allocates on the heap when the value is non-addressable.

Benchmark: Direct vs Reflective Field Access

Numbers make the cost concrete. Here's a benchmark comparing direct access to Product.Price against three reflective variants: looking up the field by name on every call, caching the field index once, and using unsafe-based field pointer arithmetic.

Run with go test -bench=. -benchmem:

A few things stand out. Direct access is under a nanosecond and allocates nothing. FieldByName is around 250x slower because of the string lookup. Switching to Field(2) by index cuts the cost to about 45x. Caching the type lookup outside the loop barely helps once you're already using an index, which tells you that the string compare in FieldByName was most of the overhead.

The 48-byte, 2-allocation cost is the boxing of product into an interface{} for reflect.ValueOf plus the reflect.Value header itself. Even the "fast" reflective path can't avoid that.

Benchmark: Calling a Method

Methods called through reflection have a similar cost profile. The benchmark below compares calling Product.Total(qty int) directly against reflect.Value.Call:

Value.Call is around 400x slower and allocates a return slice every time. Even with the method handle and argument slice prebuilt outside the loop, the call itself still has to box the arguments, dispatch through the runtime, and wrap the return values back into []reflect.Value. The boxing of arguments and unboxing of returns is the dominant cost.

Benchmark: encoding/json

The standard encoding/json package uses reflection. That's why JSON marshaling shows up in profiles of busy Go services. Here's a benchmark against a small struct to show what reflection-based serialization costs:

The handwritten version is about 6x faster and allocates half the memory. That's the gap that code generators like easyjson and ffjson aim to close: they emit code that looks like the handwritten version but is generated from the struct definition at build time.

The encoding/json package does cache type metadata internally (it has its own sync.Map of compiled type encoders), so the per-call cost is already much better than naive reflection. Most of the remaining cost is the unavoidable boxing and the generic byte buffer growth. A specialized encoder skips both.

Caching Reflection Work

The strategy that recovers most reflective overhead is the same one encoding/json uses: do the type analysis once at startup, then reuse the compiled artifacts for every call. The key insight is that reflection on a type is expensive, but reflection on a value of a type you've already analyzed is much cheaper.

Three patterns cover most cases.

Cache `reflect.Type` and `[]StructField`. Looking up field names by string is expensive. Build a map[string]int of field names to indices once per type, then use the index for every subsequent read.

sync.Once ensures the field map is built exactly one time, regardless of how many goroutines hit the function concurrently. After the first call, every lookup is a single map access plus a Field(idx).Interface() call, which is the fast reflective path from the earlier benchmark.

Cache by `reflect.Type` for multi-type code. If the same function handles many struct types, key the cache by reflect.Type itself:

The pattern is the same: walk the fields once per type, store the index map, do a single map lookup per call. sync.Map handles concurrent reads without contention because it's read-mostly.

Building a Compiled Reflection Layer

For really hot paths, the next step is to precompute everything possible during initialization and emit closures that do the minimum work per call. This is roughly how fast JSON libraries like fastjson and easyjson handle marshaling: they generate or build "compiled" accessors that know the exact offset and type of every field, then call them in sequence.

A simplified version for our Product struct that builds a list of field getters once:

The expensive work (type kind dispatch, switch statement, function value construction) happens once when compileGetters runs. Per-call, each getter does a single Field(idx) access and a type-specific operation, with no string lookups and no reflective dispatch on the type. For codes that read every field of millions of values, this is 5-10x faster than calling FieldByName per field per call.

The trade-off is complexity. The compiled layer is more code to maintain, and bugs in it are harder to debug than a straightforward FieldByName call. Most projects don't need this level of optimization. When you do, the pattern is: walk the type once, build a list of cheap closures, then call them in a loop.

When Reflection Cost Matters

Not every reflective call deserves optimization. Whether the cost matters depends entirely on how often the code runs and what else is happening on the same path.

ContextCalls per requestReflection cost matters?
Application startup (config parsing)1No, 1ms is invisible
CLI flag parsing1 per flagNo, total is sub-millisecond
ORM mapping a result row1 per rowSometimes, depends on row count
JSON unmarshal on HTTP request1 per requestUsually no for small payloads
Per-element loop over a million items1 millionYes, this is the hot path
Validation in a request handler1-10 per requestRarely, but adds up

The rule of thumb is to measure first. Profile the service under realistic load, look at where time is spent, and only optimize the paths that show up. Reflection in a startup hook or in a once-per-request configuration step doesn't matter even at 1 microsecond per call. Reflection inside a 100,000-iteration loop matters a lot.

Alternatives to Reflection

The cleanest way to make reflection fast is to not use it. Three alternatives cover most of the use cases people reach for reflection to solve.

Code generation. Tools like easyjson, ffjson, and protoc-gen-go inspect type definitions at build time and emit func (p *Product) MarshalJSON() ([]byte, error) methods directly. The generated code looks like the handwritten marshaler from the earlier benchmark and runs at the same speed. The build pipeline needs a go generate step, but the runtime cost vanishes entirely. The same reflection logic you'd write at runtime can be expressed as a code generator that runs at build time.

A typical setup:

After go generate ./..., a file product_easyjson.go appears with MarshalEasyJSON and UnmarshalEasyJSON methods. Encoding speed jumps 3-5x with no allocation overhead.

Generics (Go 1.18+). Many reflective use cases are about writing a function that works for any type. Generics solve that without runtime dispatch. The compiler stamps out a specialized version for each instantiated type, which means the code runs at native speed.

Sum runs at the same speed as a hand-written func SumFloat64([]float64) float64 because the compiler generates specialized code per instantiation. Reflection's runtime cost disappears.

Interfaces. When the variation is behavioral, not structural, a plain interface is the right answer. Define what the values need to do, not what they look like. Method calls through an interface are about 2x slower than direct calls (a pointer indirection plus a vtable lookup), but that's tiny compared to reflection.

Comparison table for the four approaches when you need to "do something across many types":

ApproachSpeed vs directType safetyWhen to use
Direct code (per type)1xCompile-timeFew types, stable schemas
Generics (Go 1.18+)1xCompile-timeBehaviorally identical across types
Interfaces~2xCompile-timeBehavior varies, structure doesn't
Code generation1xCompile-timeNeed reflection-like flexibility at zero runtime cost
Reflection (cached)30-50xRuntimeTruly unknown types at runtime (config, RPC, ORMs)
Reflection (naive)200-500xRuntimeAvoid, except in startup paths

A Brief Note on unsafe

The unsafe package can bypass reflection's allocation overhead for cases where you know the exact memory layout of the struct you're working with. The pattern uses unsafe.Pointer together with field offsets computed once at startup:

This skips the reflect.Value machinery entirely and reads the field at native speed. The cost is that unsafe is, well, unsafe: the compiler can't help you, the runtime can't help you, and a struct layout change silently produces wrong results. Tools like cgo and high-performance JSON libraries use this trick internally, but it should be the last resort, not the first.

The Go runtime itself uses similar tricks in reflect.Value.UnsafePointer and reflect.Value.UnsafeAddr, which give you a raw pointer to the underlying memory when you need it. These are documented escape hatches; the rule is to use them only when profiling proves the cost of safe reflection is the bottleneck.

Putting It Together

A realistic E-commerce scenario: a service needs to compute a search-index document for each Product by reading a configurable set of fields. The configuration is a []string of field names that arrives at startup and never changes. There are millions of products in the catalog.

The naive approach uses FieldByName on every call:

The optimized approach compiles the field accessors once at startup, then reuses them for every product:

The setup cost (one FieldByName per configured field) is paid once. After that, each product's document costs one Field(idx) plus one Interface() call per field, which is the cheapest reflective path available. For a million products and four fields, the optimized version finishes in about 200ms total; the naive version takes around 2 seconds.

Quiz

Reflection Performance Quiz

10 quizzes