13 / 13
Internals / 13

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.

The non-obvious rule. Cache reflect.Type once per type, not per value. A common pattern is 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:

OperationCostNotes
reflect.TypeOf(x)~2 nsOne pointer load. No allocation if x is already an interface.
reflect.ValueOf(x)~5 nsAllocates if x is on the stack and escape analysis fails.
v.Field(i)~10 ns + allocReads the struct field descriptor, builds a new Value.
v.Interface()~50 ns + allocAlways allocates — the data pointer plus a fresh interface header.
v.Call(args)~500 nsBuilds an argument frame, looks up the function, dispatches. Roughly 50× a direct call.
v.MethodByName("X")~100 nsLinear 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.

Further reading

Back to the hub Go runtime internals All 13 deep dives
Found this useful?