Reflection in Go: reflect.Type, reflect.Value, and When Not to Use It
Go's reflect package inspects and manipulates values at runtime when static types aren't known. reflect.TypeOf() returns the type descriptor. reflect.ValueOf() returns a Value that wraps the data. reflect.Value.CanSet() requires the value to be addressable (passed as pointer). Reflection is 10–100× slower than direct calls — use it for serialization frameworks and dependency injection, not hot paths.
reflect.Type and reflect.Value
The reflect package exposes two core types:
reflect.Type— describes the type (kind, name, fields, methods)reflect.Value— wraps a value with its type, enables read/write at runtime
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Alice", Age: 30}
// reflect.Type: inspect the type
t := reflect.TypeOf(u)
fmt.Println(t.Name()) // "User"
fmt.Println(t.Kind()) // struct
fmt.Println(t.NumField()) // 2
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(" %s %s\n", field.Name, field.Type) // Name string, Age int
}
// reflect.Value: inspect the value
v := reflect.ValueOf(u)
fmt.Println(v.Field(0).String()) // "Alice"
fmt.Println(v.Field(1).Int()) // 30
}
reflect.Kind is the primitive category: struct, ptr, slice, map, func, int, string, etc. reflect.Type.Name() is the named type (e.g., "User"). For anonymous types like []int, Name() returns "".
CanSet and addressability
To modify a value through reflection, it must be addressable — you must pass a pointer:
u := User{Name: "Alice", Age: 30}
// Cannot set — v is a copy, not addressable
v := reflect.ValueOf(u)
fmt.Println(v.Field(0).CanSet()) // false
// Can set — pass pointer, then dereference with Elem()
vp := reflect.ValueOf(&u).Elem()
fmt.Println(vp.Field(0).CanSet()) // true
vp.Field(0).SetString("Bob")
fmt.Println(u.Name) // "Bob"
The rule: reflect.ValueOf(x) where x is not a pointer gives a non-addressable value. Use reflect.ValueOf(&x).Elem() to get an addressable value that mirrors the original variable.
Struct tags
Struct tags are accessible through reflection — this is how encoding/json reads json:"name":
type Config struct {
Host string `json:"host" validate:"required"`
Port int `json:"port,omitempty"`
Timeout int `json:"timeout" default:"30"`
}
t := reflect.TypeOf(Config{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
validateTag := field.Tag.Get("validate")
fmt.Printf("%s: json=%q validate=%q\n", field.Name, jsonTag, validateTag)
}
// Host: json="host" validate="required"
// Port: json="port,omitempty" validate=""
// Timeout: json="timeout" validate=""
Calling functions dynamically
func Add(a, b int) int { return a + b }
fn := reflect.ValueOf(Add)
args := []reflect.Value{
reflect.ValueOf(3),
reflect.ValueOf(4),
}
results := fn.Call(args)
fmt.Println(results[0].Int()) // 7
Call panics if the argument count or types don't match — reflection bypasses compile-time type checking.
Reflection is 10–100× slower than direct calls — use code generation or type assertions instead for performance-sensitive code
GotchaGo / PerformanceReflection allocates heap memory for reflect.Value wrappers and cannot be inlined by the compiler. A direct struct field access is a single memory load. The same access via reflect.Value.Field(i).Int() involves multiple indirections, interface boxing, and bounds checks. For JSON marshaling at scale, encoding/json uses reflection by default but libraries like jsoniter and sonic generate specialized code at build time or use unsafe pointer arithmetic instead. The standard approach: use reflection in framework initialization (once), cache the results (reflect.Type is comparable), and use cached field offsets for hot paths.
Prerequisites
- Go interfaces
- escape analysis
- CPU caches
Key Points
- reflect.Value is an interface under the hood — every operation involves interface dispatch.
- Cache reflect.Type results: reflect.TypeOf(T{}) is cheap but repeated calls in loops waste cycles.
- unsafe.Pointer + field offsets: used by high-performance serializers to bypass reflection overhead.
- go generate: code generation (e.g., easyjson, stringer) produces type-specific code at build time, zero reflection at runtime.
Real use cases
Reflection is justified in:
-
Serialization frameworks —
encoding/json,encoding/xml,gopkg.in/yaml.v3read struct tags and iterate fields at runtime because they don't know the struct type at compile time. -
Dependency injection — frameworks like
google/wire(compile-time) oruber-go/fx(runtime) inspect constructor function signatures to resolve dependencies. -
ORM/query builders — map struct fields to database columns, handle arbitrary model types.
-
Testing utilities —
reflect.DeepEqualfor structural comparison,testify/assertfor pretty-printing differences. -
Plugin systems — loading plugins via
plugin.Open()and resolving symbols by name.
// Example: generic deep-clone using reflection
func Clone[T any](v T) T {
// For simple cases, just return a copy
// reflect needed when T contains pointers, maps, slices
src := reflect.ValueOf(v)
if src.Kind() != reflect.Ptr {
return v // value types: copy on return
}
dst := reflect.New(src.Elem().Type())
dst.Elem().Set(src.Elem())
return dst.Interface().(T)
}
You call reflect.ValueOf(myStruct).Field(0).SetString('new'). This panics. Why?
mediumreflect.ValueOf takes any interface{}. CanSet() returns false for non-addressable values.
ASetString only works on exported fields
Incorrect.Unexported fields would cause a panic with a different message ('unexported field'). But even for exported fields, non-addressable values panic with 'reflect.Value.SetString using value obtained using unexported field' — except that's also not the cause here.Breflect.ValueOf(myStruct) creates a copy — the value is non-addressable, so SetString panics with 'reflect: reflect.Value.SetString using value obtained using unexported field'
Incorrect.Close — but the panic message for non-addressability is 'reflect.Value.SetString using value obtained using unexported field' only for unexported fields. For addressability, it's different.Creflect.ValueOf(myStruct) makes a copy of the struct, and copies are non-addressable. CanSet() returns false, and SetString panics with 'reflect: reflect.Value.SetString using unaddressable value'
Correct!When you pass a value (not a pointer) to reflect.ValueOf(), Go copies it into an interface{}. Copies are non-addressable — there's no variable to write back to. CanSet() returns false. SetString() checks CanSet() and panics. Fix: pass a pointer and use Elem(): reflect.ValueOf(&myStruct).Elem().Field(0).SetString('new').Dreflect.ValueOf panics when given a struct — it only accepts primitive types
Incorrect.reflect.ValueOf accepts any value including structs, slices, maps, and pointers. The issue is not the type but the addressability.
Hint:What is the difference between passing a value vs a pointer to reflect.ValueOf? What does CanSet() check?