Reflection — Middle Level¶
Topic: Reflection Focus: The real mechanics — Go's
Type/Value/Kindand settability rules, Java'sField/Method/Constructorand the performance cliff, Python'sinspect, and why reflective calls are 10–100× slower — plus how to cache your way out.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Code Examples
- Pros & Cons
- Use Cases
- Coding Patterns
- Best Practices
- Edge Cases & Pitfalls
- Performance Notes
- Test Yourself
- Cheat Sheet
- Summary
- Further Reading
Introduction¶
Focus: How does reflection actually work in Go, Java, and Python — and what does it cost?
At the junior level, reflection was "the magic that makes JSON work." At the middle level you stop treating it as magic and start treating it as an API with sharp edges and a price tag. Three questions drive this page:
- What are the real objects? Go gives you
reflect.Typeandreflect.Value, distinguished byKind. Java gives youClass,Field,Method,Constructor. Python gives you__dict__,type(), and theinspectmodule. You need to know what each represents and how they fit together. - What are the rules that bite? Go's addressability and settability rules cause more reflective panics than anything else. Java's accessibility and the module system gate what you can touch. You need these in your bones.
- What does it cost, and how do you pay less? A reflective method call in Java can be 10–100× slower than a direct one. The fix is almost always caching: resolve the
Field/Method/struct-field once, reuse it forever.
In one sentence: the middle level is where you learn that reflection is a normal API with a settability rulebook and a performance bill, and how to keep both under control.
This page leans on concrete, runnable code. The lowest-level why (how the runtime stores type metadata, how invokedynamic rewires call sites) is in senior.md; production concerns (module system, GraalVM, security CVEs) are in professional.md.
Prerequisites¶
- Required: Comfort with
junior.md— the two halves of reflection, the type-handle entry point, struct tags vs. annotations. - Required: Solid grasp of pointers/references in your language (Go pointers especially — settability is a pointer story).
- Required: Understanding of public/private (Java) and exported/unexported (Go capitalization) visibility.
- Helpful: Having read the source of one serializer or written a tiny one.
- Helpful: Basic benchmarking experience (
go test -bench, JMH,timeit).
You do not need: the bytecode/invokedynamic internals (senior.md), or the module-system/native-image story (professional.md).
Glossary¶
| Term | Definition |
|---|---|
reflect.Type (Go) | Describes a type: its name, kind, fields, methods, tags. Comparable, usable as a map key. |
reflect.Value (Go) | Wraps an actual value and lets you read/set it (subject to rules). Pairs with a Type. |
Kind (Go) | The category of a type: Struct, Ptr, Slice, Int, String, Interface, etc. You switch on Kind constantly. Distinct from the named Type. |
| Addressable (Go) | A value has a memory address you can take. Only addressable values can be set. Values obtained from reflect.ValueOf(x) are not addressable; from reflect.ValueOf(&x).Elem() they are. |
Settable / CanSet (Go) | A Value can be assigned to. Requires addressability and an exported field. |
Elem() (Go) | Dereferences: turns a Value of a pointer into the Value it points to (and similarly for interfaces). The key to getting an addressable struct. |
Class<T> (Java) | The runtime handle for a type. Source of Field/Method/Constructor objects. |
Field / Method / Constructor (Java) | Reflective handles for a member. Carry metadata and can get/set/invoke/newInstance. |
setAccessible(true) (Java) | Disables the access check (private) on a member. Subject to the module system. |
getMethods() vs getDeclaredMethods() (Java) | getMethods: all public members including inherited. getDeclaredMethods: all members declared in this class only, any visibility. |
inspect (Python) | Standard-library module for higher-level introspection: signatures, source, members, MRO. |
__dict__ (Python) | The dictionary holding an object's (or class's) attributes. The raw substrate of Python reflection. |
| MRO | Method Resolution Order — the linearized chain Python walks to find an attribute/method across base classes. |
| Reflective dispatch | Resolving which field/method to use at runtime instead of at compile time. The source of the cost. |
| Handle caching | Resolving a Field/Method/struct-field once and reusing it, instead of looking it up on every call. |
Core Concepts¶
1. Go: Type, Value, and Kind are three different things¶
People conflate these and get confused. Keep them separate:
reflect.Typeanswers static questions: name, kind, fields, methods, tags. It doesn't hold a value.reflect.Valuewraps an actual value: you can read it (.Int(),.String(),.Interface()) and, if the rules allow, set it.Kindis the category. Two different named types (type Celsius float64,type Meters float64) have differentTypes but the sameKind(Float64). When you write generic reflective code, you almost always switch onKind, notType.
type Celsius float64
v := reflect.ValueOf(Celsius(36.6))
fmt.Println(v.Type()) // main.Celsius (the named type)
fmt.Println(v.Kind()) // float64 (the category)
The "Laws of Reflection" (Go's own framing) are worth memorizing:
- Reflection goes from interface value → reflection object (
TypeOf,ValueOf). - Reflection goes from reflection object → interface value (
.Interface()). - To modify a reflection object, the value must be settable — addressable and exported.
2. Go: addressability and settability — the rule that panics everyone¶
This is the Go reflection gotcha. Why does this panic?
Because reflect.ValueOf(u) makes a copy of u. The copy lives somewhere temporary with no address you can take — it is not addressable. Setting it would be writing into a value that's about to vanish, so Go forbids it. The fix is to reflect over a pointer and dereference with Elem():
u := User{Name: "Ada"}
v := reflect.ValueOf(&u).Elem() // now addressable
v.FieldByName("Name").SetString("Grace") // works
fmt.Println(u.Name) // "Grace"
Two conditions for CanSet() to be true:
- Addressable — you reflected through a pointer and called
.Elem(). - Exported — the field name is capitalized. Unexported fields are never settable via reflection (and only readable in limited ways). This enforces Go's encapsulation even under reflection.
3. Java: Class is the root; everything hangs off it¶
From a Class you reach the four member kinds:
Class<?> c = obj.getClass(); // or Foo.class, or Class.forName("...")
c.getDeclaredFields(); // Field[]
c.getDeclaredMethods(); // Method[]
c.getDeclaredConstructors(); // Constructor<?>[]
c.getAnnotations(); // Annotation[]
Know the getX vs getDeclaredX distinction cold:
getFields()/getMethods()→ onlypublic, but includes inherited members.getDeclaredFields()/getDeclaredMethods()→ every visibility, but only this class (not inherited).
To touch a non-public member you call setAccessible(true) — which the module system may reject (see professional.md). To create instances reflectively, prefer Constructor.newInstance() over the deprecated Class.newInstance().
4. Python: it's all dictionaries, plus inspect for the polished view¶
Python reflection is two layers:
- Raw layer:
obj.__dict__(instance attributes),type(obj).__dict__(class attributes/methods),getattr/setattr/hasattr/delattr,dir(),vars(). Everything is an object with a__dict__, so reflection is just dictionary access dressed up. - Polished layer —
inspect: higher-level helpers that hide the rough edges: inspect.signature(fn)— parameters, defaults, annotations.inspect.getmembers(obj, predicate)— filtered member listing.inspect.getsource(fn)— the actual source text.inspect.isfunction,inspect.ismethod,inspect.isclass— type predicates.type(obj).__mro__— the resolution order across base classes.
Where Go and Java make you opt in to a reflection API, Python makes reflection the path of least resistance — which is why Python frameworks lean on it heavily.
5. C#: Type plus MemberInfo hierarchy¶
obj.GetType() returns a Type; from it you get PropertyInfo, FieldInfo, MethodInfo, ConstructorInfo (all MemberInfo subclasses). Binding flags control visibility:
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo f = t.GetField("_secret", flags);
f.SetValue(obj, 42);
Activator.CreateInstance(t) constructs instances reflectively.
6. The performance story (the whole reason senior.md exists)¶
A reflective access does work a direct access never does:
- Lookup — find the
Field/Methodby name (string hashing, table walks) unless cached. - Access checks — verify visibility on each
invoke/get(mitigated bysetAccessible). - Boxing — arguments and return values get wrapped in
Object/interface{}, causing allocations. - No inlining — the JIT/compiler can't see through a reflective call, so it can't inline, devirtualize, or specialize it.
Net effect: reflective field reads and method calls commonly run 10–100× slower than direct ones, and allocate. The two universal mitigations are caching the handle and, in Java, upgrading to MethodHandle (covered in senior.md).
Real-World Analogies¶
The phone book vs. speed dial. A direct call is speed dial — the number is wired to a button. A reflective call is looking the person up in the phone book by name every single time. Caching the handle is writing their number on a sticky note after the first lookup: you still dial manually, but you skip the search.
Go's settability = renting vs. owning. reflect.ValueOf(u) hands you a photocopy of a document — you can read it, but scribbling on the copy is pointless, so the system won't let you. reflect.ValueOf(&u).Elem() hands you the original in its folder (an address), and now your edits stick. "Exported field" is the extra rule that some sections of the original are sealed (unexported) and stay read-only no matter what.
inspect as the museum tour vs. the storeroom. __dict__ is the raw storeroom — everything's there but unlabeled and messy. inspect is the curated tour: signatures, sources, and members presented cleanly. Same artifacts, friendlier access.
Mental Models¶
Model 1: "Type vs. Value vs. Kind = blueprint, brick, category." The Type is the blueprint, the Value is an actual brick you can hold (and maybe reshape), and the Kind is "it's a brick" vs. "it's a beam." Generic code dispatches on the category (Kind), reads/writes the brick (Value), and consults the blueprint (Type) for layout.
Model 2: "Settability is a chain of permissions." To write a Go field via reflection you need both keys: addressability (you came through a pointer + Elem) and export (the field is public). Miss either, and the door stays locked. Visualize a two-lock door.
Model 3: "Reflection is a tax; caching is the deduction." Every reflective op pays tax (lookup, checks, boxing). You can't avoid the tax entirely, but caching the handle removes the biggest line item (lookup), and setAccessible removes another (per-call checks). Pay once, not per request.
Model 4: "Python keeps the receipts." Python never throws away structure, so reflection is cheap to write (it's just attribute access) but still not free to run. inspect is the accountant that organizes the receipts for you.
Code Examples¶
Example 1: A reflective struct copier in Go (settability in action)¶
// CopyFields copies same-named exported fields from src to dst.
// dst must be a pointer to a struct.
func CopyFields(dst, src interface{}) {
dv := reflect.ValueOf(dst).Elem() // addressable struct
sv := reflect.ValueOf(src)
if sv.Kind() == reflect.Ptr {
sv = sv.Elem()
}
dt := dv.Type()
for i := 0; i < dt.NumField(); i++ {
name := dt.Field(i).Name
sf := sv.FieldByName(name)
df := dv.Field(i)
if sf.IsValid() && df.CanSet() && sf.Type() == df.Type() {
df.Set(sf) // only fires when addressable + exported + types match
}
}
}
Every guard here maps to a rule: IsValid (the field exists on src), CanSet (addressable + exported), and the type-equality check (no silent coercion).
Example 2: Reading struct tags the way a serializer does (Go)¶
func fieldKey(f reflect.StructField) string {
tag := f.Tag.Get("json")
if tag == "" || tag == "-" {
return f.Name // default to field name
}
if comma := strings.IndexByte(tag, ','); comma >= 0 {
tag = tag[:comma] // strip ",omitempty" etc.
}
return tag
}
This is essentially the first thing encoding/json does per field. Now the "magic" is fully demystified: it's tag parsing over reflected fields.
Example 3: Java — direct vs. reflective, and the cache that saves you¶
// SLOW: looks up the method on every call
public Object slowInvoke(Object target, String name) throws Exception {
Method m = target.getClass().getMethod(name); // lookup each time
return m.invoke(target);
}
// FAST(er): resolve once, reuse, and disable access checks
private final Map<String, Method> cache = new ConcurrentHashMap<>();
public Object cachedInvoke(Object target, String name) throws Exception {
Method m = cache.computeIfAbsent(name, n -> {
try {
Method mm = target.getClass().getMethod(n);
mm.setAccessible(true); // skip access check on each invoke
return mm;
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
return m.invoke(target);
}
The lookup is the expensive part; caching the Method removes it. setAccessible(true) additionally skips the per-invoke access check. You're still paying for boxing and the lack of inlining — senior.md shows how MethodHandle reduces even that.
Example 4: Python inspect for a real introspection task¶
import inspect
def describe(obj):
print("type:", type(obj).__name__)
print("mro :", [c.__name__ for c in type(obj).__mro__])
for name, member in inspect.getmembers(obj, inspect.ismethod):
sig = inspect.signature(member)
print(f" {name}{sig}")
class Repo:
def get(self, id: int) -> dict: ...
def save(self, item: dict, *, upsert: bool = False) -> None: ...
describe(Repo())
# type: Repo
# mro : ['Repo', 'object']
# get(id: int) -> dict
# save(item: dict, *, upsert: bool = False) -> None
inspect.signature parsing parameters, defaults, and annotations is what powers CLI generators, RPC frameworks, and pytest fixtures.
Example 5: A tiny dependency-injection move (Java)¶
// Construct an object and fill a field annotated @Inject, by reflection.
Constructor<?> ctor = type.getDeclaredConstructor();
ctor.setAccessible(true);
Object instance = ctor.newInstance();
for (Field f : type.getDeclaredFields()) {
if (f.isAnnotationPresent(Inject.class)) {
f.setAccessible(true);
f.set(instance, container.resolve(f.getType()));
}
}
This 8-line sketch is the entire idea behind Spring's field injection: reflect, find annotated members, fill them.
Pros & Cons¶
Pros
- Library-grade generality without per-type code, now with the mechanics to do it correctly.
- Tag/annotation-driven configuration keeps mapping declarative and local to the data.
inspect/signature introspection unlocks signature-aware tooling (RPC, CLIs, fixtures).
Cons (sharpened at this level)
- The settability/accessibility rulebook is easy to get wrong (Go
Elem, Java module gates). - The 10–100× cost is real and shows up in hot paths; ignoring caching is a classic perf bug.
- Allocations from boxing add GC pressure in tight reflective loops.
- Error site moves to runtime — a cached
Methodthat no longer exists fails far from where it was wired up.
Use Cases¶
- Hand-rolled serializers / mappers where you now correctly handle tags, nested structs, and settability.
- Config binding — map a
map[string]anyor env vars onto a struct via reflection + tags. - DI containers — constructor/field injection driven by annotations.
- Generic validators — walk fields, read
validate:"..."tags, apply rules. - Test/fixture frameworks —
inspect.signatureto wire arguments by name and type. - Object diffing / cloning —
CopyFields-style deep operations across unknown types.
Coding Patterns¶
Pattern 1: Resolve once, reuse forever. Cache the Field/Method/StructField keyed by (Type, name) in a ConcurrentHashMap / sync.Map. Reflection is fine on setup, deadly per request.
Pattern 2: Reflect through a pointer in Go. If you intend to set anything, start from reflect.ValueOf(&x).Elem(). Make it a habit so you never hit the addressability panic.
Pattern 3: Switch on Kind, not Type. Generic reflective walkers branch on reflect.Struct/Slice/Map/Ptr. Handle Ptr by Elem()-ing and recursing.
Pattern 4: Pre-flight every name. Before invoke/Set, verify the member exists and types match; fail with a message naming the type and member.
Pattern 5: setAccessible(true) once at cache time, not per call. It both unlocks access and removes the per-invoke check — but do it during resolution, not in the hot path.
Best Practices¶
- Build a type-info cache. Reflect a type once into a small struct (
[]fieldInfo{name, index, tag, kind}) and iterate that on every request. This single move recovers most of the lost performance. - Keep the reflective core tiny and tested. Concentrate
Set/invokein one module; everything else calls into it. - Prefer
getDeclaredX+ explicit visibility handling over guessing whichgetXreturns what. - In Go, treat unexported fields as off-limits. You can't set them and shouldn't try; redesign instead.
- Measure before and after caching. A quick benchmark proves the win and guards against regressions.
- Surface clear errors. "no method
Saveon*User" beats a rawNoSuchMethodExceptionthree layers deep.
Edge Cases & Pitfalls¶
- Go
Setpanic on a copy. Forgetting the pointer +Elem(). The #1 mistake. - Go unexported fields. Readable in limited ways, never settable via reflection;
CanSet()returns false. - Java
getMethodsvsgetDeclaredMethods. Looking for aprivateor inherited member in the wrong list returns nothing or the wrong thing. setAccessible(true)may throw. Under the module system it can fail withInaccessibleObjectException(seeprofessional.md).- Boxing surprises. Reflective numeric
set/getboxes primitives; tight loops allocate heavily. - Stale cached handles. If types are reloaded (hot reload, classloaders), a cached
Methodcan dangle. Key caches byClass, and clear on reload. - Python
dir()vs__dict__.__dict__shows only instance attributes;dir()includes class/inherited members and dunder methods. Pick the right one for the question. - MRO surprises in Python.
getattrfollows the MRO, so a subclass attribute can shadow a base one in ways that surprise reflective walkers. - Reflecting interfaces vs. concrete types (Go).
reflect.TypeOfon an interface value gives the dynamic type; a nil interface gives a nilType— guard for it.
Performance Notes¶
- The cost is dominated by lookup + checks + boxing + no-inlining. Caching removes lookup;
setAccessibleremoves checks;MethodHandle(Java) reduces the rest. - Rule of thumb: uncached reflective method call ≈ 10–100× a direct call; cached ≈ a few× to ~10×;
MethodHandle(warmed up) can approach direct-call speed. - Allocations matter as much as cycles. Boxed args/returns and reflective
[]Objectarrays churn the GC; in hot loops this often dominates. - Reflection at startup vs. steady state. Many frameworks reflect heavily at boot (scanning classes, wiring DI). That doesn't hurt throughput but does hurt startup latency — a real problem for serverless/CLI (more in
professional.md). - Benchmark honestly. Warm up the JIT, run enough iterations, and compare against a direct-call baseline — not against an unrealistic best case.
Test Yourself¶
- In Go, why does
reflect.ValueOf(u).Field(0).SetString("x")panic, and what's the fix? - What are the two conditions for
CanSet()to be true in Go? - Distinguish
Type,Value, andKind. When do you switch onKind? - In Java, what's the difference between
getMethods()andgetDeclaredMethods()? - Why is an uncached reflective call slow? List at least three contributing factors.
- What does caching a
Method/StructFieldremove from the cost, and what does it not remove? - What does
inspect.signaturegive you that raw__dict__access doesn't? - Why can reflection-heavy frameworks hurt startup latency even if steady-state throughput is fine?
Answers
1. `reflect.ValueOf(u)` copies `u`; the copy isn't addressable, so it isn't settable. Fix: `reflect.ValueOf(&u).Elem()`. 2. Addressability (came through a pointer + `Elem()`) and the field being exported (capitalized). 3. `Type` = the named type/blueprint; `Value` = an actual value you can read/maybe set; `Kind` = the category (`Struct`, `Int`...). Switch on `Kind` for generic code, since many named types share a kind. 4. `getMethods()` returns public members including inherited; `getDeclaredMethods()` returns all visibilities but only those declared on the class itself. 5. Name lookup by string, per-call access checks, boxing of args/returns (allocations), and the inability of the JIT to inline/optimize the call. 6. Caching removes the lookup; it does not remove boxing or the lack of inlining (a `MethodHandle` helps with those). 7. Parsed parameters with names, defaults, kinds (positional/keyword-only), and annotations — a structured signature, not just a list of attributes. 8. Boot-time reflection (class scanning, DI wiring) runs before serving traffic; it's pure startup cost and scales with the number of types/annotations, hurting cold-start latency.Cheat Sheet¶
| Task | Go | Java | Python |
|---|---|---|---|
| Type handle | reflect.TypeOf(x) | x.getClass() | type(x) |
| Value handle | reflect.ValueOf(x) | (the object) | (the object) |
| Make settable | reflect.ValueOf(&x).Elem() | f.setAccessible(true) | (always mutable) |
| List fields | t.Field(i), t.NumField() | getDeclaredFields() | x.__dict__, vars(x) |
| Read field | v.Field(i) | f.get(x) | getattr(x,"f") |
| Set field | v.Field(i).Set(...) (settable!) | f.set(x,v) | setattr(x,"f",v) |
| Read tag/meta | f.Tag.Get("json") | annotations | decorators |
| Invoke method | v.MethodByName("M").Call(...) | m.invoke(x,...) | getattr(x,"m")(...) |
| Signature | (parse manually) | m.getParameterTypes() | inspect.signature(m) |
Go laws: ① interface→reflect ② reflect→interface ③ to set, be settable (addressable + exported). Perf rule: cache the handle; setAccessible once; benchmark against a direct-call baseline.
Summary¶
The middle level turns reflection from magic into a mechanical, costed API. In Go you juggle Type, Value, and Kind, and you live or die by addressability and settability: reflect through a pointer and call Elem(), and remember only exported, addressable fields are settable. In Java everything hangs off Class, you must know getX vs. getDeclaredX, and setAccessible(true) unlocks (and speeds up) member access. In Python, reflection is just __dict__ plus the friendly inspect module for signatures and members.
The unifying theme is cost: uncached reflective access runs 10–100× slower because of lookup, access checks, boxing, and the loss of inlining. The fix is nearly universal — resolve handles once and cache them, set accessibility at cache time, and keep reflection out of hot paths. Master the settability rulebook and the caching discipline, and you can use reflection confidently. The next level explains why it's slow at the runtime/JIT level and how MethodHandle/invokedynamic claw the performance back.
Further Reading¶
- Go: "The Laws of Reflection" and the
reflectpackage docs; readencoding/json'sencode.gofield-caching for a production pattern. - Java:
java.lang.reflectAPI,Classmethods, and theAccessibleObjectcontract. - Python: the
inspectmodule reference and the data-model chapter on attributes and the MRO. - Then continue to
senior.mdfor the runtime mechanics,MethodHandle/LambdaMetafactory/invokedynamic, and lock-free reflective dispatch.
In this topic
- junior
- middle
- senior
- professional