Reflection Advanced¶
Introduction¶
Reflection in Go allows a program to inspect and manipulate its own types and values at runtime. The reflect package provides this capability, enabling you to examine struct fields, read struct tags, call functions dynamically, and build types on the fly.
Reflection is the engine behind Go's standard library: encoding/json, encoding/xml, fmt.Println, database/sql, and text/template all rely heavily on it. Understanding reflection is essential for writing libraries and frameworks — but equally important is knowing when not to use it.
Syntax & Usage¶
reflect.Type and reflect.Value¶
Every Go value has two components that reflection exposes:
reflect.Type— the type of the value (struct, int, func, etc.)reflect.Value— the value itself, wrapped in a reflection container
import "reflect"
x := 42
s := "hello"
u := User{Name: "Alice", Age: 30}
// reflect.TypeOf returns the reflect.Type
fmt.Println(reflect.TypeOf(x)) // int
fmt.Println(reflect.TypeOf(s)) // string
fmt.Println(reflect.TypeOf(u)) // main.User
// reflect.ValueOf returns the reflect.Value
fmt.Println(reflect.ValueOf(x)) // 42
fmt.Println(reflect.ValueOf(s)) // hello
fmt.Println(reflect.ValueOf(u)) // {Alice 30}
Kind vs Type¶
Type is the specific Go type (main.User, int, []string).
Kind is the category of type (struct, int, slice).
type UserID int64
var id UserID = 42
t := reflect.TypeOf(id)
fmt.Println(t) // main.UserID (the Type)
fmt.Println(t.Kind()) // int64 (the Kind — the underlying category)
fmt.Println(t.Name()) // UserID
// Kind is one of the constants in reflect package:
// Bool, Int, Int8, ..., Float32, Float64,
// String, Array, Slice, Map, Chan, Func,
// Ptr, Struct, Interface, UnsafePointer
Inspecting Struct Fields and Tags¶
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"email"`
Age int `json:"age,omitempty" validate:"gte=0,lte=150"`
IsAdmin bool `json:"-"` // Excluded from JSON
created time.Time // Unexported — not accessible via reflection from other packages
}
func inspectStruct(v interface{}) {
t := reflect.TypeOf(v)
// If pointer, dereference to get the struct type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
fmt.Println("not a struct")
return
}
fmt.Printf("Struct: %s (%d fields)\n", t.Name(), t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(" Field: %-10s Type: %-12s", field.Name, field.Type)
// Read struct tags
if jsonTag, ok := field.Tag.Lookup("json"); ok {
fmt.Printf(" json:%q", jsonTag)
}
if validateTag, ok := field.Tag.Lookup("validate"); ok {
fmt.Printf(" validate:%q", validateTag)
}
fmt.Printf(" Exported: %v\n", field.IsExported())
}
}
// Output:
// Struct: User (5 fields)
// Field: Name Type: string json:"name" validate:"required" Exported: true
// Field: Email Type: string json:"email" validate:"email" Exported: true
// Field: Age Type: int json:"age,omitempty" validate:"gte=0,lte=150" Exported: true
// Field: IsAdmin Type: bool json:"-" Exported: true
// Field: created Type: time.Time Exported: false
Setting Values (Must Be Addressable)¶
// reflect.Value can set values, but only if the value is ADDRESSABLE.
// This requires passing a pointer.
x := 42
v := reflect.ValueOf(x)
// v.SetInt(100) // PANICS — v is not addressable (it's a copy)
// Pass a pointer, then call Elem() to get the addressable value
v = reflect.ValueOf(&x).Elem()
v.SetInt(100)
fmt.Println(x) // 100
// Setting struct fields
type Config struct {
Host string
Port int
}
cfg := Config{Host: "localhost", Port: 8080}
v = reflect.ValueOf(&cfg).Elem()
hostField := v.FieldByName("Host")
if hostField.IsValid() && hostField.CanSet() {
hostField.SetString("0.0.0.0")
}
portField := v.FieldByName("Port")
if portField.IsValid() && portField.CanSet() {
portField.SetInt(9090)
}
fmt.Println(cfg) // {0.0.0.0 9090}
Calling Functions Dynamically¶
func add(a, b int) int { return a + b }
func greet(name string) string { return "Hello, " + name }
func callDynamic(fn interface{}, args ...interface{}) []interface{} {
v := reflect.ValueOf(fn)
if v.Kind() != reflect.Func {
panic("not a function")
}
// Convert args to reflect.Value
in := make([]reflect.Value, len(args))
for i, arg := range args {
in[i] = reflect.ValueOf(arg)
}
// Call the function
results := v.Call(in)
// Convert results back to interface{}
out := make([]interface{}, len(results))
for i, r := range results {
out[i] = r.Interface()
}
return out
}
result := callDynamic(add, 3, 4)
fmt.Println(result[0]) // 7
result = callDynamic(greet, "World")
fmt.Println(result[0]) // Hello, World
The Three Laws of Reflection¶
Rob Pike's three laws define the relationship between Go values and reflection:
// Law 1: Reflection goes from interface value to reflection object.
// reflect.TypeOf and reflect.ValueOf accept interface{} and return
// reflect.Type / reflect.Value.
var x float64 = 3.14
v := reflect.ValueOf(x) // Go value → reflect.Value
// Law 2: Reflection goes from reflection object to interface value.
// reflect.Value.Interface() returns the value as interface{}.
y := v.Interface().(float64) // reflect.Value → Go value
fmt.Println(y) // 3.14
// Law 3: To modify a reflection object, the value must be settable.
// Settable means the reflect.Value holds the ADDRESS of the original variable.
v = reflect.ValueOf(&x).Elem() // Must pass pointer, then Elem()
v.SetFloat(2.71)
fmt.Println(x) // 2.71
Practical Example: Struct Tag Parser (Validation Library)¶
type ValidationError struct {
Field string
Tag string
Message string
}
func Validate(v interface{}) []ValidationError {
var errors []ValidationError
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
typ = typ.Elem()
}
if val.Kind() != reflect.Struct {
return errors
}
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
tag := field.Tag.Get("validate")
if tag == "" || !field.IsExported() {
continue
}
rules := strings.Split(tag, ",")
for _, rule := range rules {
if err := applyRule(field.Name, value, rule); err != nil {
errors = append(errors, *err)
}
}
}
return errors
}
func applyRule(fieldName string, value reflect.Value, rule string) *ValidationError {
switch {
case rule == "required":
if value.IsZero() {
return &ValidationError{
Field: fieldName,
Tag: "required",
Message: fmt.Sprintf("%s is required", fieldName),
}
}
case strings.HasPrefix(rule, "min="):
minStr := strings.TrimPrefix(rule, "min=")
min, _ := strconv.Atoi(minStr)
switch value.Kind() {
case reflect.String:
if value.Len() < min {
return &ValidationError{
Field: fieldName,
Tag: "min",
Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min),
}
}
case reflect.Int, reflect.Int64:
if value.Int() < int64(min) {
return &ValidationError{
Field: fieldName,
Tag: "min",
Message: fmt.Sprintf("%s must be at least %d", fieldName, min),
}
}
}
case strings.HasPrefix(rule, "max="):
maxStr := strings.TrimPrefix(rule, "max=")
max, _ := strconv.Atoi(maxStr)
switch value.Kind() {
case reflect.String:
if value.Len() > max {
return &ValidationError{
Field: fieldName,
Tag: "max",
Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max),
}
}
case reflect.Int, reflect.Int64:
if value.Int() > int64(max) {
return &ValidationError{
Field: fieldName,
Tag: "max",
Message: fmt.Sprintf("%s must be at most %d", fieldName, max),
}
}
}
}
return nil
}
// Usage
type CreateUserRequest struct {
Name string `validate:"required,min=2,max=50"`
Email string `validate:"required"`
Age int `validate:"min=0,max=150"`
}
req := CreateUserRequest{Name: "A", Age: 200}
errs := Validate(req)
for _, e := range errs {
fmt.Printf("%s: %s\n", e.Field, e.Message)
}
// Name: Name must be at least 2 characters
// Email: Email is required
// Age: Age must be at most 150
Practical Example: Generic JSON-to-Map Converter¶
func StructToMap(v interface{}) map[string]interface{} {
result := make(map[string]interface{})
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
typ = typ.Elem()
}
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
if !field.IsExported() {
continue
}
// Use json tag name if available
name := field.Name
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] == "-" {
continue
}
if parts[0] != "" {
name = parts[0]
}
// Handle omitempty
if len(parts) > 1 && parts[1] == "omitempty" && val.Field(i).IsZero() {
continue
}
}
result[name] = val.Field(i).Interface()
}
return result
}
Inspecting Interface Implementations at Runtime¶
var errorType = reflect.TypeOf((*error)(nil)).Elem()
var stringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
func implementsError(v interface{}) bool {
return reflect.TypeOf(v).Implements(errorType)
}
func implementsStringer(v interface{}) bool {
t := reflect.TypeOf(v)
return t.Implements(stringerType) || reflect.PtrTo(t).Implements(stringerType)
}
Quick Reference¶
| Operation | Function/Method | Notes |
|---|---|---|
| Get type | reflect.TypeOf(v) |
Returns reflect.Type |
| Get value | reflect.ValueOf(v) |
Returns reflect.Value |
| Type category | t.Kind() |
reflect.Struct, reflect.Int, etc. |
| Type name | t.Name() |
"User", "" for unnamed types |
| Struct fields | t.NumField(), t.Field(i) |
Returns reflect.StructField |
| Field by name | t.FieldByName("Name") |
Also available on reflect.Value |
| Struct tag | field.Tag.Get("json") |
Returns tag value string |
| Set value | v.SetInt(42), v.SetString("") |
Must be addressable (use & + Elem()) |
| Settable? | v.CanSet() |
Check before calling Set* |
| Interface | v.Interface() |
Convert reflect.Value → interface{} |
| Pointer elem | v.Elem() |
Dereference pointer or interface |
| Call function | v.Call(args) |
args is []reflect.Value |
| Create pointer | reflect.New(t) |
Like new(T) — returns *T as reflect.Value |
| Make slice | reflect.MakeSlice(t, len, cap) |
Dynamic slice creation |
| Make map | reflect.MakeMap(t) |
Dynamic map creation |
Best Practices¶
-
Exhaust all alternatives before using reflection — generics (1.18+), interfaces, and code generation are almost always better.
-
Cache reflect.Type —
reflect.TypeOfitself is cheap, but repeatedly computing types in hot loops adds up. Store types in package-level variables. -
Check
CanSet()before setting — attempting to set a non-settable value panics. -
Handle pointers at entry points — always check
Kind() == reflect.Ptrand callElem()at the start of reflection functions. -
Use
field.IsExported()— never attempt to get or set unexported fields from outside the package (it panics). -
Write comprehensive tests — reflection code is hard to reason about statically. Test with diverse types including edge cases (nil, zero values, nested structs).
Common Pitfalls¶
Panic on Non-Settable Values
Panic on Wrong Kind Operations
reflect.DeepEqual Gotchas
// nil slice vs empty slice
var s1 []int // nil
s2 := []int{} // empty, non-nil
reflect.DeepEqual(s1, s2) // false! Different from most expectations
// Time comparison ignores wall clock
t1 := time.Now()
t2 := t1.Round(0) // Strips monotonic clock reading
reflect.DeepEqual(t1, t2) // false! Use t1.Equal(t2) instead
// Unexported fields are compared
// This means two structs with different unexported fields are not equal,
// even if all exported fields match
Interface vs Concrete Type Confusion
var err error = fmt.Errorf("oops")
// TypeOf sees the INTERFACE, not the concrete type
// Actually, TypeOf sees through the interface to the concrete type
fmt.Println(reflect.TypeOf(err)) // *errors.errorString
fmt.Println(reflect.TypeOf(err).Kind()) // ptr
// But if you want to check the interface type itself:
// You need the pointer-to-interface trick
var errType = reflect.TypeOf((*error)(nil)).Elem()
fmt.Println(errType) // error
Performance Considerations¶
Reflection is 10–100x slower than direct code for equivalent operations:
// Benchmark: direct field access vs reflection
func BenchmarkDirect(b *testing.B) {
u := User{Name: "Alice"}
for i := 0; i < b.N; i++ {
_ = u.Name // ~0.3 ns
}
}
func BenchmarkReflection(b *testing.B) {
u := User{Name: "Alice"}
v := reflect.ValueOf(u)
for i := 0; i < b.N; i++ {
_ = v.FieldByName("Name").String() // ~50-100 ns
}
}
func BenchmarkReflectionCached(b *testing.B) {
u := User{Name: "Alice"}
v := reflect.ValueOf(u)
t := v.Type()
nameIdx, _ := t.FieldByName("Name")
for i := 0; i < b.N; i++ {
_ = v.FieldByIndex(nameIdx.Index).String() // ~20-30 ns (faster with cached index)
}
}
Strategies to reduce reflection cost:
-
Cache field indices and types — compute them once at init time, not per-call.
-
Use code generation —
go generatewith tools likeeasyjson,msgp, orstringerproduces non-reflective code. -
Use generics (Go 1.18+) — many patterns that previously required reflection (type-safe containers, serialization helpers) can now use generics.
-
Limit reflection to startup/init — parse struct tags and build lookup tables at init time, use the tables at runtime.
// Example: cache struct metadata at init time
type fieldInfo struct {
index int
jsonName string
required bool
}
var typeCache sync.Map // map[reflect.Type][]fieldInfo
func getFieldInfo(t reflect.Type) []fieldInfo {
if cached, ok := typeCache.Load(t); ok {
return cached.([]fieldInfo)
}
// Compute and cache
var fields []fieldInfo
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
info := fieldInfo{index: i}
if tag := f.Tag.Get("json"); tag != "" {
parts := strings.Split(tag, ",")
info.jsonName = parts[0]
}
if tag := f.Tag.Get("validate"); strings.Contains(tag, "required") {
info.required = true
}
fields = append(fields, info)
}
typeCache.Store(t, fields)
return fields
}
Generics vs Reflection (When to Use Which)¶
| Scenario | Use Generics | Use Reflection |
|---|---|---|
| Type-safe container (Stack, Set) | ✅ | ❌ |
| Functional utilities (Map, Filter) | ✅ | ❌ |
| JSON serialization/deserialization | ❌ | ✅ (struct tags) |
| ORM field mapping | ❌ | ✅ (struct tags) |
| Validation library | ❌ | ✅ (struct tags) |
fmt.Println-like formatting |
❌ | ✅ (arbitrary types) |
| Testing deep equality | ❌ | ✅ (reflect.DeepEqual) |
| Plugin/middleware systems | ❌ | ✅ (dynamic dispatch) |
Interview Tips¶
Interview Tip
When asked about reflection, emphasize the trade-offs: it sacrifices compile-time type safety and performance for runtime flexibility. A good answer is: "I use reflection for libraries that must handle arbitrary user-defined types — like JSON serialization or validation — but never in application-level code where generics or interfaces suffice."
Interview Tip
The three laws of reflection (interface → reflection object, reflection object → interface, settability requires addressability) are frequently asked in interviews. Know them and be able to explain the third law with a pointer example.
Interview Tip
If asked "How does encoding/json work?", explain: it uses reflect.TypeOf to inspect struct fields, reads json struct tags for field naming, and uses reflect.ValueOf to get/set field values. Mention that this is why JSON marshaling is ~10x slower than code-generated alternatives like easyjson.
Interview Tip
A strong answer includes knowing when NOT to use reflection: "Since Go 1.18, generics handle most of the type-safe abstraction use cases that previously required reflection. I'd only reach for reflection when I need to inspect struct tags, handle truly unknown types at runtime, or build framework-level utilities."
Key Takeaways¶
- Reflection enables runtime type inspection and manipulation via
reflect.Typeandreflect.Value. - Kind is the type category (struct, int, slice); Type is the specific Go type (User, int64).
- Values must be addressable (passed by pointer) to be set via reflection.
- Reflection is 10–100x slower than direct code — cache metadata, limit to init paths.
- Generics replace most reflection use cases since Go 1.18 — reflection remains necessary for struct tags, arbitrary type handling, and framework internals.
- The
encoding/json,database/sql, andfmtpackages are built on reflection — understanding it helps you understand the standard library.