Reflection
Reflection is the runtime asking "what type is this, and what can I do with it?" at
execution time. Every Go binary carries a small descriptor for every type that's ever
assigned to an interface; reflect exposes that machinery. The cost is real
— each reflect.Value typically allocates, and method dispatch through reflection is an
order of magnitude slower than a direct call — which is why the runtime offers it and
the standard library uses it sparingly.
runtime._type — the type descriptor
Every concrete type in a Go program has a runtime._type descriptor: size,
alignment, kind, hash function, equality function, GC bitmap, name, and method set.
These descriptors are emitted by the compiler into the binary's read-only data section
and stay there for the program's lifetime. An interface value is two words — a pointer
to one of these descriptors, plus a pointer to the underlying data.
// runtime/type.go (simplified)
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // GC bitmap
str nameOff
ptrToThis typeOff
}reflect.Type is a thin wrapper around a pointer to one of these. Calling
reflect.TypeOf(x) on an interface value just reads the type-descriptor
pointer out of the interface header — no allocation, no copy. That part is cheap.
reflect.Value — wrapping the data pointer
reflect.Value packages three things: a pointer to the type descriptor,
a pointer to the data, and a flag word encoding kind and addressability. Most
operations on a Value go through v.kind(), which switches on the type
descriptor's kind field to dispatch to the right code path.
Where it gets expensive: every Value returned by a navigation operation (Field,
Index, MapIndex) is a fresh struct. The runtime usually has
to allocate on the heap because the result escapes through the reflect interface, and
the escape-analysis pass can't prove otherwise. A loop that walks a struct's fields
via reflection generally allocates one Value per field, per iteration.
typeOfX = reflect.TypeOf(X{}) at package init, then
reuse the cached Type for every later operation. The Value escape is harder to avoid,
but skipping the redundant TypeOf calls is free.Where the standard library uses reflection
Three big ones — encoding/json, encoding/gob, and
text/template. Each takes an interface{} and uses reflection
to walk fields, look up tags, marshal/unmarshal, or render. encoding/json
amortises the cost by caching a per-type encoder; first marshal of a new type is
expensive (reflect the whole struct, build an encoder), every subsequent marshal of
the same type uses the cached encoder and is much faster.
This caching pattern is the right shape for most reflect-using libraries: reflect once to build a closure or code path specialised to the type, then use that fast path on every subsequent value. The cost is paid once per type, not once per value.
Performance numbers
Rough order-of-magnitude figures on modern x86, current Go versions:
| Operation | Cost | Notes |
|---|---|---|
reflect.TypeOf(x) | ~2 ns | One pointer load. No allocation if x is already an interface. |
reflect.ValueOf(x) | ~5 ns | Allocates if x is on the stack and escape analysis fails. |
v.Field(i) | ~10 ns + alloc | Reads the struct field descriptor, builds a new Value. |
v.Interface() | ~50 ns + alloc | Always allocates — the data pointer plus a fresh interface header. |
v.Call(args) | ~500 ns | Builds an argument frame, looks up the function, dispatches. Roughly 50× a direct call. |
v.MethodByName("X") | ~100 ns | Linear scan over the method table. Cache the result. |
The conclusion: reflection on a cold path (config parsing at startup, JSON marshaling of a complex struct once per request) is free. Reflection in a tight loop (per-row in a query, per-tick in a game loop) is a serious cost and worth eliminating.
When generics removed the need
A large fraction of pre-1.18 reflection was working around the lack of generics. Generic collections, generic functional helpers, slice/map transforms — all the cases where "the caller knows the type but the library function doesn't" — used to require reflect. Generics took most of those use cases away, with a fraction of the runtime cost.
Reflection still wins for dynamic cases: serialisation of types the library
has never seen, template engines that walk arbitrary data, RPC frameworks that
marshal an interface{}. For those, no static type system can help — the
data shape is only known at runtime.