How Go's reflect Package Reads Types: The rtype and the Method Table
reflect is not a parallel type system: it reads the very same runtime type descriptors the compiler emits, and a reflect.Value is just a (typ, ptr, flag) triple with a bitfield packed into the flags word.
TL;DR:
reflectdoes not have its own type system. It reads the exactabi.Typedescriptors the compiler emits into the binary — the same ones the garbage collector and the runtime use. Areflect.Valueis a three-word triple: a type pointer, a data pointer, and a flag word where ten bits carry the kind, whether the data is behind a pointer, whether it is addressable, and whether it is read-only.
Reflection in Go has a reputation for being slow and imprecise. Both accusations are half right, and the reasons are worth understanding, because the implementation is not what most people assume.
The usual assumption is that reflect maintains a parallel, richer
description of every type — a “runtime type” distinct from the “compile-time
type”. It doesn’t. There is exactly one type description in a Go binary: the
compiler emits it, it is linked into read-only data, and both the runtime
and reflect read it.
This is how it all works, based on reading the Go source tree (Go 1.28
development tree, commit c0be4674).
One type description, two readers
The canonical structure lives in src/internal/abi/type.go, because the
runtime and the compiler must agree on its layout byte for byte:
// src/internal/abi/type.go:21
type Type struct {
Size_ uintptr
PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
Hash uint32 // hash of type; avoids computation in hash tables
TFlag TFlag // extra type information flags
Align_ uint8 // alignment of variable with this type
FieldAlign_ uint8 // alignment of struct field with this type
Kind_ Kind // what kind of type this is (string, int, ...)
// function for comparing objects of this typeTwenty-odd lines that describe every type in the program: how big it is, how much of it contains pointers (what the GC cares about), a hash for map keys, an equality function, the GC’s pointer bitmask, and two offsets into the module’s name and type tables.
reflect does not have its own copy. It aliases it:
// src/reflect/type.go:345
// Embed this type to get common/uncommon
type common struct {
abi.Type
}
// rtype is the common implementation of most values.So reflect.rtype is a one-field wrapper around abi.Type. The Type
interface is implemented by *rtype and its friends (structType,
ptrType, sliceType, …), each a larger struct whose first field is
the same abi.Type header. That is why the conversion is a single cast:
// src/reflect/type.go:780
func toRType(t *abi.Type) *rtype {
return (*rtype)(unsafe.Pointer(t))
}A slice’s type descriptor is a sliceType whose first bytes happen to be a
Type header. Cast to *rtype and you get the header; cast to *sliceType
and you get the whole thing. One representation, many views.
The bridge from the runtime’s world to reflect’s world is one linknamed
function:
// src/reflect/type.go:2845
//go:linkname toType
func toType(t *abi.Type) Type {
if t == nil {
return nil
}
return toRType(t)
}The runtime version returns a *rtype; the reflect version returns the
public Type interface. Same memory, different static type.
The flag bitfield
abi.Type carries a TFlag byte:
// src/internal/abi/type.go:110
// TFlagNamed means the type has a name.
TFlagNamed TFlag = 1 << 2
// TFlagRegularMemory means that equal and hash functions can treat
// this type as a single region of t.size bytes.
TFlagRegularMemory TFlag = 1 << 3
// TFlagGCMaskOnDemand means that the GC pointer bitmask will beThe design idea here is worth stealing: the common part of a type
descriptor is fixed-size and shared by every type of that kind; the
variable parts sit immediately after it, with a bit in TFlag telling you
whether they are there at all.
TFlagUncommon is the important one: it says “the optional UncommonType
struct follows the common header”. TFlagExtraStar is a binary-size
optimisation — the name stored for *T may be the name of T with a *
glued on, and the flag tells you to strip it. TFlagRegularMemory lets
reflect.DeepEqual and the runtime’s map code take a shortcut: a type with
no internal structure compares as raw bytes. TFlagDirectIface is the
answer to “does this value fit in an interface word?”.
The method table
Where do methods live? Not in the Type header — that is shared by every
type of that kind, including ones with no methods at all. Methods live in
an optional trailer:
// src/internal/abi/type.go:230
type UncommonType struct {
PkgPath NameOff // import path; empty for built-in types like int, string
Mcount uint16 // number of methods
Xcount uint16 // number of exported methods
Moff uint32 // offset from this uncommontype to [mcount]Method
_ uint32 // unused
}Moff is an offset, not a pointer, because the method array sits right
after the UncommonType struct. Methods() reads it with unchecked
arithmetic:
// src/internal/abi/type.go:238
func (t *UncommonType) Methods() []Method {
if t.Mcount == 0 {
return nil
}
return (*[1 << 16]Method)(addChecked(unsafe.Pointer(t), uintptr(t.Moff), "t.mcount > 0"))[:t.Mcount:t.Mcount]
}addChecked looks like a bounds check but isn’t — the string argument is
documentation only, and the function exists so the compiler can still inline
p+x.
Note the two text offsets. Tfn is the function you would call directly,
receiver first. Ifn is the interface version — the wrapper an itab
points at.
reflect reads this table to build its Method values:
// src/reflect/type.go:645
func (t *rtype) Method(i int) (m Method) {
if t.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(t))
return tt.Method(i)
}
methods := t.exportedMethods()
if i < 0 || i >= len(methods) {
panic("reflect: Method index out of range")
}
p := methods[i]
pname := t.nameOff(p.Name)
m.Name = pname.Name()That is a lot of work per call: a new funcType with a receiver prepended,
allocated slices, a fresh Value. The “reflection is slow” story in
miniature — and it is only slow because it is general.
reflect.Value: a triple with a bitfield
reflect.Value is the most over- and under-appreciated struct in the
standard library. Here is the whole thing:
// src/reflect/value.go:40
type Value struct {
// typ_ holds the type of the value represented by a Value.
// Access using the typ method to avoid escape of v.
typ_ *abi.Type
// Pointer-valued data or, if flagIndir is set, pointer to data.
// Valid when either flagIndir is set or typ.pointers() is true.
ptr unsafe.PointerThree words: type, data pointer, flags. The flags word packs everything
reflect needs to know about a value without dereferencing the type:
| Bit | Meaning |
|---|---|
| 0–4 | The Kind of the value, cached so v.Kind() is a mask, not a load |
| 5 | flagStickyRO — came from an unexported non-embedded field |
| 6 | flagEmbedRO — came from an unexported embedded field |
| 7 | flagIndir — ptr points at the data rather than being the data |
| 8 | flagAddr — the value is addressable (CanAddr) |
| 9 | flagMethod — the value is a bound method value |
flagIndir is the subtle one. A reflect.Value holding an int stores
the integer in the ptr word, because an int fits in a pointer-sized
word. A reflect.Value holding a string — two words — cannot, so it
stores a pointer to the string header and sets flagIndir.
The read-only flags prevent a whole class of bugs: unsafe can read
unexported fields, but reflect refuses to hand them back through
Interface():
// src/reflect/value.go:1486
func (v Value) Interface() (i any) {
return valueInterface(v, true)
}
func valueInterface(v Value, safe bool) any {
if v.flag == 0 {
panic(&ValueError{"reflect.Value.Interface", Invalid})
}CanSet is the same idea in miniature: v.flag&(flagAddr|flagRO) == flagAddr — addressable and not read-only, both conditions in one
expression.
packEface: turning a Value back into an any
Value.Interface() has to produce an any, which is a two-word structure:
a type pointer and a data pointer. packEface constructs the header
directly:
// src/reflect/value.go:118
// packEface converts v to the empty interface.
func packEface(v Value) any {
return *(*any)(unsafe.Pointer(&abi.EmptyInterface{
Type: v.typ(),
Data: packEfaceData(v),
}))
}Three cases, and the middle one is the interesting one. If the type is
“direct interface” (fits in a word, no pointers — int, bool, a pointer,
a chan) but the Value was obtained indirectly, the data has to be
loaded out of the pointed-to location. If the type is not direct but the
Value is addressable, the data has to be copied into a fresh
allocation: the interface owns that word and the original might move.
And going the other way, unpackEface is the whole of ValueOf:
// src/reflect/value.go:153
// unpackEface converts the empty interface i to a Value.
func unpackEface(i any) Value {
e := (*abi.EmptyInterface)(unsafe.Pointer(&i))
t := e.Type
if t == nil {
return Value{}
}
f := flag(t.Kind())That is the whole trick: read the type pointer out of the interface header,
read the kind out of the type descriptor, set flagIndir if the type does
not fit directly, and you are done.
reflect.TypeOf is an interface conversion
Here is the part that surprises people. reflect.TypeOf does no work at
runtime beyond what the language already did for you:
// src/reflect/type.go:1389
// TypeOf returns the reflection [Type] that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i any) Type {
return toType(abi.TypeOf(i))
}and abi.TypeOf is a reinterpretation of the interface header:
// src/internal/abi/type.go:181
// TypeOf returns the abi.Type of some value.
func TypeOf(a any) *Type {
eface := *(*EmptyInterface)(unsafe.Pointer(&a))
// Types are either static (for compiler-created types) or
// heap-allocated but always reachable (for reflection-created
// types, held in the central map). So there is no need to
// escape types. noescape here help avoid unnecessary escape
// of v.
return (*Type)(NoEscape(unsafe.Pointer(eface.Type)))
}Passing a value to reflect.TypeOf(i any) boxes it into an interface, and
the interface header already contains a pointer to the type descriptor.
TypeOf hands that pointer back. No lookup, no map, no allocation.
reflect.ValueOf is slightly more work: it constructs the flag word,
which means inspecting IsDirectIface.
And reflect.TypeOf(nil) returns nil, because a nil interface has a nil
type pointer. There is no “nil type”.
The itab: interface dispatch, for contrast
The other half of type identity is the itab, the table that makes
err.(someInterface) work. It’s defined in src/internal/abi/iface.go:
// src/internal/abi/iface.go:17
type ITab struct {
Inter *InterfaceType
Type *Type
Hash uint32 // copy of Type.Hash. Used for type switches.
Fun [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter.
}A non-empty interface value is a pair: an *ITab and a data pointer. The
ITab says which concrete type is inside, which interface it matched
against, and — in the variable-length Fun array — the function pointers
for each method.
// src/runtime/iface.go:44
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.Methods) == 0 {
throw("internal error - misuse of itab")
}
// easy case
if typ.TFlag&abi.TFlagUncommon == 0 {
if canfail {
return nilNote the first check: if the concrete type has no methods, the
TFlagUncommon bit is clear, and the type cannot implement any non-empty
interface. The flag doing real work.
Type switches never consult the runtime hash table. The compiler statically
generates an itab for every (interface, concrete type) pair in a type
switch, so the switch is a hash comparison and a lookup in a
compiler-built table — no locks, no allocation, no runtime call.
Reflection versus a type switch
The performance difference is not “reflection is slow and type switches are fast” in the abstract. It’s concrete and measurable:
| Operation | What actually happens |
|---|---|
switch v.(type) { case T: } | Compiler-generated hash comparison, then a table lookup. No allocation, no interface call. |
reflect.TypeOf(v) == reflect.TypeOf(x) | Read the type pointer out of the interface header, compare two pointers. Essentially free. |
reflect.ValueOf(v).Field(i) | Build a Value, compute offsets, check bounds, construct a new Value with new flags. |
reflect.ValueOf(v).Interface() | packEface — possibly a heap allocation and a typedmemmove. |
reflect.ValueOf(v).Call(args) | Allocate an argument frame, translate each argument through the ABI description, call through a function pointer, translate results back. |
reflect.TypeOf(v).Method(i) | Rebuild a funcType, allocate slices, allocate a Value. |
So the “reflection is slow” complaint is really two complaints: reflection
forces allocation (the boxed any, the reconstructed Method.Type), and
reflection loses static knowledge (the compiler knows your struct’s field
offsets; reflect has to look them up).
Use reflection at API boundaries, in tests, in serializers, in dependency injection glue — places where the types genuinely aren’t known at compile time.
Trade-offs
What the shared descriptor buys:
- One copy of the truth. The runtime and
reflectcannot disagree about a type’s size, alignment, or pointer layout, because there is only one description of it. - Small binaries. Every type is one descriptor in read-only data.
reflectadds no parallel metadata. - The GC gets what it needs for free.
GCDatais in the same structure.
What it costs:
- The descriptor serves everybody.
Equalis a function pointer so==works for non-comparable types;GCDatahas a mode flag for types whose pointer mask is computed on demand. - Layout access is unsafe. Everything past the simple accessors is
unsafe.Pointercasts and offsets. - The flag word is a compromise. Caching the
Kindmakesv.Kind()a single instruction, but aValuecan be constructed wrong.reflectguards withmustBe,mustBeExported, and a pile ofpanics. - Method lookup rebuilds signatures. There is no pre-built signature to point at.
Caveats
The Type interface is not the runtime’s Type. reflect.Type is an
interface with about forty methods; abi.Type is a struct. They are
related by an unsafe.Pointer cast, not by Go’s type system.
reflect.TypeOf and reflect.ValueOf are not symmetric. TypeOf
returns nil for a nil interface; ValueOf returns the zero Value,
which panics if you call most methods on it.
The Kind cache in the flag word can go stale in principle. It can’t in
practice, because a Value’s type never changes — but a hand-constructed
Value with a wrong flag word misbehaves in confusing ways.
Unexported fields behave oddly. v.Field(i) on an unexported field
returns a Value with flagStickyRO set: readable with Int(),
String() and friends, but Interface() and Set() panic.
Method(i) only returns exported methods for non-interface types, and
NumMethod reports the exported count.
reflect.Type values are comparable but reflect.Value values are not.
== on two Values compares the struct fields, not the values they
represent.
Wrapping up
The one thing to remember: reflect is not a parallel type system. It
is a set of accessors over the compiler-emitted abi.Type descriptors, and
reflect.Value is a (typ, ptr, flag) triple whose flag word carries the
kind, the indirection, the addressability and the read-only state in ten
bits. Once you see that, “reflection is slow” becomes a specific claim:
reflection allocates, and it looks things up at runtime.
The part people get wrong: they assume reflect.TypeOf has to look
anything up. It doesn’t. Boxing a value into an any already put a pointer
to the type descriptor in the interface header; TypeOf reads it back out.
The expensive parts of reflection are the ones that have to build
something — a Method’s signature, an argument frame, a boxed result.
What to go do about it: next time you reach for reflect in a hot path,
check whether you actually need it. reflect.TypeOf(x) == reflect.TypeOf(y)
is a pointer comparison and costs nothing. A type switch is cheaper still.
Reserve reflect.Value.Call and reflect.Type.Method for boundaries where
the types are dynamic.
Further reading
src/internal/abi/type.go— theTypestruct,TFlagbits,UncommonType,Methodsrc/internal/abi/iface.go—ITab,EmptyInterface,NonEmptyInterfacesrc/reflect/type.go—rtype, theTypeinterface,toType,TypeOf,Methodsrc/reflect/value.go— theValuestruct, theflagbitfield,packEface,unpackEfacesrc/runtime/iface.go—getitaband theitabcachesrc/runtime/type.go— the runtime’s own view of the same descriptorssrc/cmd/compile/internal/reflectdata/reflect.go— where the compiler emits them