Reflect Internals and Cost
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:
Value.Interface()boxingValueOfof small values that escape- Building
[]reflect.ValueforCall - Pathological use inside tight loops without type caches
// Cache field indexes by type
var cache sync.Map // reflect.Type -> []int indicesencoding/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=. -benchmempackage 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_testAlternatively 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=. -benchmemWhat 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).