Reflection in Go provides runtime access to type information and value manipulation, but it comes with performance and safety trade-offs. This article explores the core concepts of reflection—reflect.Type, reflect.Value, and Kind—and demonstrates practical applications with slices, maps, structs, pointers, and functions.
Core Concepts: Type, Value, and Kind
Reflecsion in Go revolves around two primary types and a concept called Kind:
reflect.Type: Represents the metadata of any Go type, including its name, package path, methods, and underlying Kind. It provides static type information but does not allow direct value manipulation.reflect.Value: Represents a runtime value, encapsulating both its type and its data. It enables reading and, when addressable, modifying the underlying value.- Kind: An enumeration (
reflect.Kind) that categorizes a type or value into a specific class, such asreflect.Int,reflect.String,reflect.Slice,reflect.Struct,reflect.Ptr, orreflect.Interface. It covers both primitive and composite types.
For example:
var x int = 10
t := reflect.TypeOf(x) // t is of type reflect.Type, representing int
v := reflect.ValueOf(x) // v is of type reflect.Value, holding 10
k := v.Kind() // k is reflect.Int
Reflection with Slices
A slice in Go contains a pointer to an underlying array, a length, and a capacity. Reflection enables dynamic inspection and modification of slices.
// Underlying slice header structure
type sliceHeader struct {
Data uintptr
Len int
Cap int
}
Basic Operations
s := []int{1, 2, 3}
typ := reflect.TypeOf(s) // []int
val := reflect.ValueOf(s) // value of slice
Accessing and Modifying Elements
Reflection allows reading and, if the value is settable, modifying slice elements.
if val.CanSet() {
elem := val.Index(0)
elem.SetInt(42) // changes first element to 42
}
Replacing the Entire Slice
To replace the whole slice, use Set with a new slice of the same type.
newSlice := []int{4, 5, 6}
val.Set(reflect.ValueOf(newSlice))
Length and Capacity
length := val.Len()
capacity := val.Cap()
Note that reflection does not provide a direct append equivalent; a new slice must be created and elements copied manually.
Reflection with Maps
Maps in Go are hash-based key-value collections. Reflection allows dynamic type inspection and content manipulation.
Checking Map Type
m := map[string]int{"a": 1}
typ := reflect.TypeOf(m)
if typ.Kind() == reflect.Map {
keyType := typ.Key() // string
valueType := typ.Elem() // int
}
Iterating and Modifying Entries
val := reflect.ValueOf(m)
for _, key := range val.MapKeys() {
value := val.MapIndex(key)
fmt.Println("Key:", key.Interface(), "Value:", value.Interface())
// Modify value (must be settable)
if value.CanSet() {
val.SetMapIndex(key, reflect.ValueOf(2))
}
}
Creating New Map Instances
Reflection can create new map instances dynamically when the key and value type are known at runtime.
Reflection with Structs
Structs are composite types with named fields. Reflection provides powerful runtime introspection and manipulation.
Accessing Type and Value
type Person struct {
Name string
Age int
}
p := Person{"Alice", 30}
typ := reflect.TypeOf(p)
val := reflect.ValueOf(p)
Iterating Fields
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
value := val.Field(i)
fmt.Printf("Field: %s, Type: %v, Value: %v\n", field.Name, field.Type, value.Interface())
}
Reading and Modifying Field Values
nameField := val.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
nameField.SetString("Bob")
}
Working with Struct Tags
Tags (e.g., JSON) are accessible via reflection.
tag := typ.Field(0).Tag.Get("json")
fmt.Println("JSON tag:", tag)
Calling Struct Methods
Methods can be discovered and invoked dynamically.
Reflection with Pointers
Pointers enable indirect memory access. Combined with reflection, they enable modification of underlying values even when passed as interfaces.
Dereferencing Pointers
var i int = 42
ptr := &i
v := reflect.ValueOf(ptr).Elem() // dereferences the pointer
v.SetInt(1337)
fmt.Println(i) // 1337
Modifying Private Struct Fields via Pointer
Reflection can access unexported fields if a pointer to the struct is used.
type Data struct {
secret string
}
d := &Data{"initial"}
v := reflect.ValueOf(d).Elem()
field := v.FieldByName("secret")
if field.IsValid() && field.CanSet() {
field.SetString("modified")
}
Creating New Pointer Values
type MyType struct{}
newInstance := reflect.New(reflect.TypeOf(MyType{})).Elem()
Reflection with Functions
Reflection enables dynamic function invocation and signature inspection.
Getting Function Type
add := func(a, b int) int { return a + b }
typ := reflect.TypeOf(add)
fmt.Println(typ) // func(int, int) int
Calling a Function Dynamically
fn := reflect.ValueOf(add)
params := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(5)}
results := fn.Call(params)
fmt.Println(results[0].Interface()) // 8
Inspecting Function Signature
numIn := typ.NumIn()
numOut := typ.NumOut()
for i := 0; i < numIn; i++ {
fmt.Println("Input type:", typ.In(i))
}
for i := 0; i < numOut; i++ {
fmt.Println("Output type:", typ.Out(i))
}
Handling Methods
Reflection can identify receiver types of methods.
type T struct{}
func (T) M() {}
m := reflect.ValueOf(T.M)
methodType := m.Type()
if methodType.NumIn() > 0 {
fmt.Println("Receiver type:", methodType.In(0))
}
Summary of Reflection Capabilities
Reflection in Go allows runtime access to:
- Type metadata (name, kind, fields, methods, tags)
- Value reading and modification (when addressable)
- Dynamic method calls
- Creating new instances of types
- Iterating over collections (slices, maps, arrays)
- Cross-package access to unexported members (discouraged)
While reflection adds significant flexibility for generic utilities, serialization frameworks, and dynamic dispatch, it should be used sparingly due to performance costs and potential safety risks.