跪拜 Guibai
← All articles
Backend · Go

Go's reflect Package: Three Patterns for Runtime Type, Value, and Method Access

By 小满zs ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Reflection shifts errors from compile time to runtime and adds overhead, so these three patterns separate safe reflect-based utilities from panic-prone code. Most production Go projects keep reflection confined to framework and serialization layers, and the advice to prefer interfaces and generics holds for everyday business logic.

Summary

Reflection is a standard-library power tool used by frameworks, ORMs, and JSON codecs. It trades compile-time safety for runtime flexibility, so Go's usual advice applies: prefer interfaces or generics for business logic, and reach for reflection only when a library or framework demands it.

Type inspection comes first. reflect.TypeOf returns a Type; Name() gives the declared name (like "Age"), while Kind() gives the underlying built-in kind (reflect.Int). Branch on Kind, because it stays stable across named types and aliases. Mutation is next: reflect.ValueOf(&x).Elem() yields a settable value; check CanSet, IsValid, and the expected Kind before calling SetInt. To get a typed value back, val.Interface().(Age) performs a type assertion. Method invocation is the third pattern: MethodByName("Say") finds an exported method, and Call takes a slice of reflect.Value arguments, returning a slice of reflect.Value results.

These patterns boil down to: use Type for reading, Value for writing; pass a pointer and call Elem to mutate; box values with ValueOf on the way in, and unbox with Interface() plus a type assertion on the way out. Those three moves cover the reflect usage that appears in real code.

Takeaways
— reflect.TypeOf returns a reflect.Type describing the value's type.
— t.Name() gives the declared type name (e.g., "Age"), while t.Kind() gives the underlying kind (e.g., reflect.Int).
— Branch on Kind rather than Name when checking what a value actually is.
— To modify a value through reflection, pass a pointer and call Elem() to get a settable reflect.Value.
— Check CanSet(), IsValid(), and Kind before calling SetInt or other setters to avoid panics.
— val.Interface().(Age) performs a type assertion to restore the static type.
— MethodByName("Say") finds an exported method; it returns an invalid Value if the method is not found.
— Call takes a slice of reflect.Value arguments; signature mismatches cause a panic.
— Exported method names (capitalized) are required for MethodByName to work.
Conclusions

The Name/Kind split is the first trap: Name reports the custom type identifier, but Kind reports the underlying representation, so any branch that switches on Kind works for all named types built on that same base.

Passing a pointer and calling Elem() is the only way to get a settable reflect.Value; skipping Elem() yields a copy that will panic on SetInt. That design forces developers to be explicit about mutating the original.

Preferring interfaces and generics over reflection is not just performance advice; it keeps type errors at compile time, where the compiler can catch them, instead of allowing them to surface as runtime panics.

Concepts & terms
Reflection
Runtime inspection and manipulation of a program's types, values, and methods, provided by the reflect package. It enables generic code but sacrifices compile-time type safety and adds runtime cost.
Name vs. Kind
Type.Name() returns the declared type name (e.g., 'Age' for a custom int), while Type.Kind() returns the underlying built-in type (reflect.Int). Branching on Kind is more reliable because it ignores named-type wrappers.
Elem()
A method on reflect.Value that dereferences a pointer, returning a reflect.Value that can be modified. It is required to make a reflected value settable; without it, the value is a copy and Set* calls panic.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗