Skip to content

Reflection

The reflect package provides runtime type introspection and manipulation. It allows programs to Examine types, inspect struct fields, call methods by name, and modify values dynamically.

Reflection is powerful but slow and type-unsafe. Use it only when static typing is insufficient: Serialization, ORMs, configuration parsing, testing frameworks.

The two core types:

  • reflect.Type represents a Go type (int, string, struct, slice, etc.)
  • reflect.Value represents a value of any type
func inspect(i any) {
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
fmt.Println("Type:", t)
fmt.Println("Kind:", t.Kind())
fmt.Println("Value:", v)
}
inspect(42) // Type: int, Kind: int, Value: 42
inspect("hello") // Type: string, Kind: string, Value: hello
inspect(3.14) // Type: float64, Kind: float64, Value: 3.14

Kind is the underlying category. Type is the concrete type:

type MyInt int
var x MyInt = 42
t := reflect.TypeOf(x)
fmt.Println(t.Name()) // MyInt
fmt.Println(t.Kind()) // int

Kind is one of: Bool``Int``Int8``Int16``Int32``Int64``Uint``Uint8``Uint16 Uint32``Uint64``Float32``Float64``Complex64``Complex128``String``Array``Slice Map``Chan``Func``Interface``Struct``Ptr``UnsafePointer.

reflect.ValueOf returns a non-settable value (a copy). To modify the original, pass a pointer:

x := 42
v := reflect.ValueOf(&x).Elem() // dereference the pointer to get a settable value
v.SetInt(100)
fmt.Println(x) // 100

CanSet() indicates whether a value can be modified:

v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // false -- value was copied
v = reflect.ValueOf(&x).Elem()
fmt.Println(v.CanSet()) // true -- points to the original
type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"min=0"`
}
p := Person{Name: "Alice", Age: 30}
t := reflect.TypeOf(p)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, Type: %s, Tag: %s\n",
field.Name, field.Type, field.Tag.Get("json"))
}
// Field: Name, Type: string, Tag: name
// Field: Age, Type: int, Tag: age
p := Person{}
v := reflect.ValueOf(&p).Elem()
nameField := v.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
nameField.SetString("Bob")
}
ageField := v.FieldByName("Age")
if ageField.IsValid() && ageField.CanSet() {
ageField.SetInt(25)
}
fmt.Println(p) // {Bob 25}

Struct tags are key-value metadata attached to struct fields. They are conventionally used by Libraries for serialization, validation, and ORM mapping:

type Config struct {
Host string `toml:"host" default:"localhost"`
Port int `toml:"port" default:"8080"`
Debug bool `toml:"debug" default:"false"`
}

Access tags via reflection:

field, _, _ := reflect.TypeOf(Config{}).FieldByName("Host")
tag := field.Tag
fmt.Println(tag.Get("toml")) // "host"
fmt.Println(tag.Get("default")) // "localhost"
sliceType := reflect.TypeOf([]int{})
slice := reflect.MakeSlice(sliceType, 0, 10)
slice = reflect.Append(slice, reflect.ValueOf(1))
slice = reflect.Append(slice, reflect.ValueOf(2))
fmt.Println(slice.Interface()) // [1 2]
mapType := reflect.TypeOf(map[string]int{})
m := reflect.MakeMap(mapType)
m.SetMapIndex(reflect.ValueOf("a"), reflect.ValueOf(1))
m.SetMapIndex(reflect.ValueOf("b"), reflect.ValueOf(2))
fmt.Println(m.Interface()) // map[a:1 b:2]
fn := reflect.ValueOf(strings.ToUpper)
result := fn.Call([]reflect.Value{reflect.ValueOf("hello")})
fmt.Println(result[0].Interface()) // "HELLO"

For functions with variadic arguments, use CallSlice:

fn := reflect.ValueOf(fmt.Sprintf)
result := fn.Call([]reflect.Value{
reflect.ValueOf("%s %d"),
reflect.ValueOf("count"),
reflect.ValueOf(42),
})

Convert an any to a concrete type using reflection:

func ToInt(i any) (int, bool) {
v := reflect.ValueOf(i)
if v.Kind() == reflect.Int {
return int(v.Int()), true
}
return 0, false
}

Prefer type assertions over reflection when the type is known at compile time:

// Better: type assertion
n, ok := i.(int)
// Worse: reflection (slower, no compile-time checking)
v := reflect.ValueOf(i)

Reflection enables generic processing of arbitrary types:

func PrintFields(v any) {
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
fmt.Println("not a struct")
return
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
fmt.Printf("%s (%s): %v\n", field.Name, field.Type, value.Interface())
}
}

Reflection is significantly slower than direct access:

// Direct: ~1ns
p.Name = "Alice"
// Reflection: ~100ns (100x slower)
reflect.ValueOf(&p).Elem().FieldByName("Name").SetString("Alice")

If performance is critical, avoid reflection. Consider code generation (text/template) or generics As alternatives.

Reflection is the runtime looking in a mirror: Normally, Go’s type system knows everything at compile time — what’s an int, what’s a string, what fields a struct has. Reflection is the ability to ask “what am I?” at runtime. It’s like opening a box and reading its label instead of knowing what’s inside from the packing slip. Struct tags are sticky notes on those boxes that serialization libraries read to know how to pack/unpack them.

Why it matters: Reflection enables libraries that work with arbitrary types — JSON encoders, ORM mappers, validation frameworks — without requiring code generation. It’s the price of admission for writing truly generic code in a statically typed language.

The key insight: Reflection is powerful but expensive — it trades compile-time safety and performance for runtime flexibility. Use generics or type assertions first; reach for reflection only when you genuinely don’t know the type.

  1. Reflecting on unexported fields. Reflection cannot read or set unexported (lowercase) struct fields. CanSet() returns false for unexported fields.

  2. Forgetting Elem() for pointer values. reflect.ValueOf(&x) gives you a *int Value. Call .Elem() to get the underlying int Value for modification.

  3. Panicking on wrong types. Int() panics if the kind is not an integer. Always check Kind() before calling type-specific methods.

  4. Performance in hot paths. Reflection is 10-100x slower than direct code. Do not use it in tight loops. Cache reflect.Type and reflect.Value results when possible.

  5. Using reflection when type assertions suffice. If you know the possible types at compile time, use type switches and type assertions. They are type-safe and fast.

  6. Modifying unexported fields. This is not possible through the reflect package. If you must do it, use unsafe (but this is highly discouraged and may break across Go versions).

  7. Ignoring CanAddr and CanSet. Not all reflect.Value objects are addressable or settable. Always check before attempting modification.

This topic covers the core concepts of reflection, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

  • Interfaces: Interface satisfaction and type assertions underlying reflection.
  • Generics: Compile-time type parameters as an alternative to runtime reflection.
  • Types and Variables: Go’s type system fundamentals that reflection inspects at runtime.