Reflection in Go

Reflection is a program’s ability to examine and manipulate its own values and types at run time — to ask of a value it received as an opaque interface{} “what type are you, what fields do you have, what is your third field’s value, set it to 7.” Go provides this through the standard reflect package. Rob Pike’s foundational blog post “The Laws of Reflection” (go.dev/blog/laws-of-reflection) frames the whole package around three rules governing the conversions between interface values and reflection objects. The single most important idea: reflection is not magic — it is a typed API over the exact (type, value) pair that every interface value already carries. reflect.TypeOf and reflect.ValueOf simply read the two words of an interface and hand them back as inspectable reflect.Type and reflect.Value objects. Reflection is therefore as powerful as it is because every value, the moment it is assigned to an interface{}, carries its full type descriptor along with it.

Mental Model

The connective tissue between Go’s static type system and run-time introspection is the empty interface. A variable of type any (the Go 1.18 alias for interface{}) is a two-word box: one word points to a runtime._type descriptor, the other to the data. Reflection is the API that opens that box. reflect.Type is a view of the first word; reflect.Value is a view of the box as a whole. Everything else — walking struct fields, calling methods dynamically, building new slices — is built on those two views.

graph LR
    V["concrete value<br/>e.g. User{Name:&quot;A&quot;}"] -->|"assign to any"| I["interface value<br/>(type word, data word)"]
    I -->|"reflect.TypeOf"| T["reflect.Type<br/>(Law 1)"]
    I -->|"reflect.ValueOf"| RV["reflect.Value<br/>(Law 1)"]
    RV -->|".Interface()"| I2["interface value<br/>(Law 2)"]
    RV -->|"pointer + .Elem()<br/>+ .CanSet()"| MOD["modifies real storage<br/>(Law 3)"]
    I2 -->|"type assertion"| V2["concrete value back"]

Diagram: the cycle the three laws describe. Law 1 is the conversion into reflection (interface → reflect.Type/reflect.Value). Law 2 is the conversion out (reflect.Value → interface, recoverable to a concrete value by assertion). Law 3 governs mutation — only a reflect.Value derived from a pointer’s Elem() is settable. The insight: reflection is a round-trip, and you can only enter it from an interface value, because the interface is where the type information lives.

The Three Laws of Reflection

Rob Pike’s post states them verbatim:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

Law 1 — Interface value to reflection object

reflect.TypeOf(i any) reflect.Type returns a reflect.Type describing the dynamic type stored in i. reflect.ValueOf(i any) reflect.Value returns a reflect.Value holding a copy of the run-time value. Both take any, so calling them with a concrete value first implicitly boxes it into an interface — that boxing is the entry point.

A reflect.Type’s most important method is Kind() reflect.Kind. Kind is not Type. Type is the specific named type (main.Celsius); Kind is the underlying category it belongs to (reflect.Float64). For type Celsius float64, Type.String() is "main.Celsius" but Kind() is reflect.Float64. Reflection code branches on Kind because the category determines which methods are legal: you may call .Float() on any float-kind value regardless of its named type. The Kind constants enumerate every category — Bool, the sized integers, Float32/64, Complex64/128, Array, Chan, Func, Interface, Map, Pointer, Slice, String, Struct, UnsafePointer, plus Invalid for the zero reflect.Value.

Law 2 — Reflection object back to interface value

func (v reflect.Value) Interface() any repackages a reflect.Value into an interface{} — the inverse of ValueOf. The result’s static type is always any; to get a usable concrete value you type-assert it (v.Interface().(float64)). This is how reflection-based libraries hand a constructed value back to ordinary code: build it dynamically, then Interface() it out.

Law 3 — Settability

Law 3 is the subtle one. A reflect.Value is settable only if it refers to real, addressable storage rather than a copy. reflect.ValueOf(x) copies x into the Value; calling v.SetFloat(...) on it panics — “using unaddressable value” — because there is no original to write back to. The fix is to reflect a pointer and indirect: reflect.ValueOf(&x).Elem() produces a Value that is x’s storage and reports CanSet() == true. For struct fields, the same rule plus an extra one: only exported fields are settable, because reflection respects the language’s visibility rules. v.CanSet() is the always-available predicate; check it before any Set* call to avoid the panic.

Mechanical Walk-through

What actually happens at the machine level — and why this couples tightly to Interface Internals: an interface value is two pointer-sized words. The first is the type word: a *runtime._type (for empty interfaces) or a *runtime.itab (for non-empty interfaces, where the itab embeds the _type). The second is the data word: a pointer to the value, or the value itself if it is pointer-shaped.

reflect.TypeOf extracts the type word and returns it wrapped behind the reflect.Type interface. reflect.ValueOf captures both words into the reflect.Value struct (which internally holds a *rtype, a data pointer, and a flag bitset recording kind, addressability, and read-only status). No new type information is created — reflection is pure reading of the descriptor the compiler already emitted into the binary for that type. This is why reflection cannot see information the compiler never recorded: it cannot recover a function’s local-variable names, and it cannot read unexported fields’ values without unsafe tricks, because the visibility flag is part of the descriptor.

Method dispatch via reflection (Value.Method(i).Call(args)) walks the type’s method table — the same table an itab indexes — locates the function pointer, marshals the []reflect.Value arguments into a call frame matching the function’s signature, and invokes it through the runtime’s reflectcall. This is genuinely dynamic dispatch arranged by hand, and it is why reflective calls are markedly slower than direct calls: argument marshalling and an indirect call replace what would otherwise be a single CALL instruction.

Code Examples

Walking a struct generically

package main
 
import (
	"fmt"
	"reflect"
)
 
type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
	pw   string // unexported
}
 
func dump(x any) {
	v := reflect.ValueOf(x)        // line 1: box x, get a Value
	t := v.Type()                  // line 2: its dynamic Type
	if t.Kind() != reflect.Struct { // line 3: branch on Kind, not Type
		fmt.Println("not a struct")
		return
	}
	for i := 0; i < t.NumField(); i++ { // line 4: iterate declared fields
		f := t.Field(i)            // line 5: StructField — metadata
		fv := v.Field(i)           // line 6: the field's Value
		tag := f.Tag.Get("json")   // line 7: parse the struct tag
		fmt.Printf("%s (%s) tag=%q exported=%v\n",
			f.Name, fv.Kind(), tag, f.IsExported()) // line 8
	}
}
 
func main() {
	dump(User{Name: "Ada", Age: 36, pw: "secret"})
}
  • Line 1reflect.ValueOf(x) implicitly boxes x into any, then reads the two interface words. v is not settable: x was passed by value.
  • Line 2v.Type() returns the dynamic reflect.Type; equivalently reflect.TypeOf(x).
  • Line 3Kind() returns reflect.Struct for any struct type. Branching on Kind (not on a specific named Type) keeps the function generic across all struct types.
  • Lines 4–6NumField/Field on the Type yield StructField metadata (name, type, tag, offset); the same Field index on the Value yields the run-time field value.
  • Line 7f.Tag is a reflect.StructTag; .Get("json") parses the `json:"name"` tag. This is exactly how encoding/json discovers field-to-key mappings.
  • Line 8f.IsExported() (added Go 1.17) reports visibility; reflecting the value of the unexported pw would require unsafe.

Settable values through a pointer

func main() {
	var x float64 = 3.4
	reflect.ValueOf(x).SetFloat(7.1)        // PANICS: unaddressable
 
	v := reflect.ValueOf(&x).Elem()          // line A
	fmt.Println(v.CanSet())                  // true
	v.SetFloat(7.1)                          // line B
	fmt.Println(x)                           // 7.1
}
  • The panic linereflect.ValueOf(x) holds a copy of x; there is no storage to write through, so SetFloat panics with “using unaddressable value”. This is Law 3.
  • Line Areflect.ValueOf(&x) reflects the pointer; .Elem() dereferences it, yielding a Value that aliases x’s actual storage. CanSet() now returns true.
  • Line BSetFloat writes through to x itself.

Constructing values dynamically

t := reflect.TypeOf(0)                       // reflect.Type for int
slice := reflect.MakeSlice(reflect.SliceOf(t), 3, 3) // []int len/cap 3
slice.Index(0).SetInt(42)
out := slice.Interface().([]int)             // Law 2: back to []int
fmt.Println(out)                             // [42 0 0]

reflect.SliceOf, MakeSlice, New, MakeMap, and FuncOf build types and values that never appeared literally in the source — the mechanism behind decoders that materialize arbitrary target types.

Iterating a map reflectively

A map’s key/value types are not known statically inside generic reflection code, so reflect provides a dedicated iterator, *reflect.MapIter:

v := reflect.ValueOf(map[string]int{"a": 1, "b": 2})
iter := v.MapRange()                 // line 1: returns *MapIter
for iter.Next() {                    // line 2: advance; false when exhausted
	k := iter.Key()                  // line 3: current key as a Value
	val := iter.Value()              // line 4: current value as a Value
	fmt.Println(k.String(), val.Int())
}
  • Line 1MapRange() returns a *MapIter positioned before the first entry.
  • Line 2Next() advances and reports whether an entry exists. After it returns false, calling Key, Value, or Next again panics — the iterator is single-use unless Reset is called.
  • Lines 3–4Key()/Value() yield reflect.Values; MapRange follows the same iteration semantics (including order randomization) as a range statement. MapIter.Reset(v) re-points an existing iterator at a new map, and Reset(reflect.Value{}) detaches it so the previously iterated map can be garbage-collected.

Calling a function dynamically

reflect.Value.Call invokes a function whose signature is only known at run time:

fn := reflect.ValueOf(strings.Repeat)             // a func(string,int) string
args := []reflect.Value{
	reflect.ValueOf("ab"),
	reflect.ValueOf(3),
}
out := fn.Call(args)                              // []reflect.Value
fmt.Println(out[0].String())                      // "ababab"

Call requires that fn’s Kind is Func; that len(args) matches the parameter count; and that each argument is assignable (per Type Identity and Assignability) to the corresponding parameter type — any mismatch panics. For a variadic function, Call automatically packs the trailing arguments into the variadic slice, while CallSlice instead takes the slice ready-made as the final argument. Call also panics if fn was obtained from an unexported struct field. The return is a []reflect.Value, one entry per result.

reflect.DeepEqual — Recursive Equality

reflect.DeepEqual(x, y any) bool is the most-used utility in the package — == cannot compare slices, maps, or functions, and DeepEqual fills that gap by being, in the documentation’s words, “a recursive relaxation of Go’s == operator.” The rules per kind: arrays and structs are deeply equal when every element/field (exported and unexported) is deeply equal; slices are deeply equal when both are nil or both non-nil, lengths match, and corresponding elements are deeply equal — note []byte{} and []byte(nil) are not deeply equal because one is nil and one is not; maps require both-nil-or-both-non-nil, equal length, and deeply-equal values for matching keys; pointers are deeply equal if ==-equal or if they point to deeply-equal values; functions are deeply equal only if both are nil. To stay terminating on cyclic structures, “the second and subsequent times that DeepEqual compares two pointer values that have been compared before, it treats the values as equal” — a cycle is assumed equal rather than chased forever. One sharp edge: DeepEqual can report a value unequal to itself, because a func-typed field is never equal to anything and a floating-point NaN is never equal to itself. DeepEqual is invaluable in tests but should not be a hot-path equality check — it reflects and recurses over the entire structure.

Failure Modes and Common Misunderstandings

Confusing Kind and Type. Kind() of a type MyID int is reflect.Int; its Type is MyID. Code that switches on Type when it means category, or vice versa, misbehaves on named types. Switch on Kind for “what can I do with this,” compare Type for “is this exactly this type.”

The unaddressable-value panic. Set* on a non-pointer-derived Value panics. Always reflect &x and .Elem(), and gate every Set* with CanSet().

Unexported fields. You can see an unexported field’s metadata (NumField counts it, Field(i) returns it) but .Interface() on it panics (“cannot return value obtained from unexported field”) and it is never settable. Libraries that reach into unexported fields use unsafe — see The unsafe Package.

Performance. Reflection replaces compiler-known offsets and direct calls with run-time lookups, argument boxing, and indirect dispatch. Hot paths that reflect per element are a classic profiling finding; the standard fix is to reflect once to build a plan (field offsets, setters) and then execute the plan without further reflection — what encoding/json does with its per-type encoder cache.

Nil and the zero Value. reflect.ValueOf(nil) returns an Invalid-kind Value whose IsValid() is false; most methods panic on it. Check IsValid() when the input may be nil.

Alternatives and When to Choose Them

Since Go 1.18, generics replace many former uses of reflection: a generic Map[T,U] or slices.Contains is type-safe, fast, and needs no reflection. Reach for reflection only when the set of types genuinely is not known until run time — serialization (encoding/json, encoding/gob), object-relational mappers, dependency-injection containers, deep-equality (reflect.DeepEqual), and test frameworks. For “I just need to know if this satisfies an interface,” a type assertion or type switch is faster and clearer than reflection. The rule of thumb the Go community repeats: “reflection is never clear” — prefer generics, then type switches, and use reflect only when neither can express the problem.

Production Notes

encoding/json is the canonical production user: it reflects each struct type once on first encounter, builds and caches an encoder closure keyed by reflect.Type, and reuses it — amortizing the reflection cost. Recent releases keep extending the package: reflect.TypeFor[T]() (Go 1.22) gets a Type from a type parameter without an instance; Value.Seq/Seq2 and reflect.SliceAt (Go 1.23) integrate reflection with range-over-function iterators; reflect.TypeAssert[T](v) (Go 1.25) does a generic, panic-free assertion straight from a reflect.Value; and Value.Fields/Value.Methods (Go 1.26, each returning iter.Seq2) let you range directly over a value’s fields and methods. The Go 1.26 release notes confirm these last two verbatim — “the new methods Value.Fields and Value.Methods return iterators over a value’s fields or methods, respectively. Each iteration yields the type information (StructField or Method) of a field or method, along with the field or method Value — alongside their type-level counterparts Type.Fields, Type.Methods, Type.Ins, and Type.Outs (Go 1.26 release notes, reflect section). As of the Go 1.26 baseline (released 10 February 2026, with 1.26.3 the current patch as of 7 May 2026, per the release history) these are all current.

See Also