Reflection in Go: Type Inspection
Reflection allows inspecting and manipulating types and values at runtime. While powerful, it should be used carefully as it adds complexity and overhead.
Reflection Basics
The reflect package provides runtime type information.
Good: Basic Type Inspection
package main
import (
"fmt"
"reflect"
)
func inspectType(v interface{}) {
t := reflect.TypeOf(v)
fmt.Printf("Type: %v\n", t)
fmt.Printf("Kind: %v\n", t.Kind())
fmt.Printf("Name: %v\n", t.Name())
}
func main() {
inspectType(42)
inspectType("hello")
inspectType([]int{1, 2, 3})
inspectType(map[string]int{"a": 1})
}
Bad: Unnecessary Reflection
// โ AVOID: Using reflection when not needed
package main
import (
"fmt"
"reflect"
)
func getValue(v interface{}) interface{} {
// Unnecessary reflection
return reflect.ValueOf(v).Interface()
}
func main() {
result := getValue(42)
fmt.Println(result)
}
Type Assertion vs Reflection
Good: Type Assertion (Preferred)
package main
import (
"fmt"
)
func processValue(v interface{}) {
switch val := v.(type) {
case int:
fmt.Printf("Integer: %d\n", val)
case string:
fmt.Printf("String: %s\n", val)
case []int:
fmt.Printf("Slice: %v\n", val)
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
processValue(42)
processValue("hello")
processValue([]int{1, 2, 3})
}
Bad: Reflection When Type Assertion Works
// โ AVOID: Using reflection for simple type checking
package main
import (
"fmt"
"reflect"
)
func processValue(v interface{}) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Int {
fmt.Printf("Integer: %d\n", v)
} else if t.Kind() == reflect.String {
fmt.Printf("String: %s\n", v)
}
}
func main() {
processValue(42)
processValue("hello")
}
Inspecting Structs
Examine struct fields and tags.
Good: Struct Field Inspection
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name" db:"person_name"`
Age int `json:"age" db:"person_age"`
Email string `json:"email"`
}
func inspectStruct(v interface{}) {
t := reflect.TypeOf(v)
if t.Kind() != reflect.Struct {
fmt.Println("Not a struct")
return
}
fmt.Printf("Struct: %s\n", t.Name())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(" Field: %s, Type: %v\n", field.Name, field.Type)
fmt.Printf(" JSON tag: %s\n", field.Tag.Get("json"))
fmt.Printf(" DB tag: %s\n", field.Tag.Get("db"))
}
}
func main() {
p := Person{Name: "Alice", Age: 30, Email: "[email protected]"}
inspectStruct(p)
}
Inspecting Values
Examine and modify values at runtime.
Good: Value Inspection
package main
import (
"fmt"
"reflect"
)
func inspectValue(v interface{}) {
val := reflect.ValueOf(v)
fmt.Printf("Type: %v\n", val.Type())
fmt.Printf("Kind: %v\n", val.Kind())
fmt.Printf("Value: %v\n", val.Interface())
fmt.Printf("Can Set: %v\n", val.CanSet())
}
func main() {
x := 42
inspectValue(x)
// Pointer allows modification
inspectValue(&x)
}
Modifying Values
package main
import (
"fmt"
"reflect"
)
func modifyValue(v interface{}) {
val := reflect.ValueOf(v)
// Must be pointer to modify
if val.Kind() != reflect.Ptr {
fmt.Println("Not a pointer")
return
}
elem := val.Elem()
if elem.Kind() == reflect.Int {
elem.SetInt(100)
}
}
func main() {
x := 42
fmt.Printf("Before: %d\n", x)
modifyValue(&x)
fmt.Printf("After: %d\n", x)
}
Calling Functions Dynamically
Invoke functions using reflection.
Good: Dynamic Function Call
package main
import (
"fmt"
"reflect"
)
func add(a, b int) int {
return a + b
}
func callFunction(fn interface{}, args ...interface{}) []interface{} {
f := reflect.ValueOf(fn)
// Convert args to reflect.Value
reflectArgs := make([]reflect.Value, len(args))
for i, arg := range args {
reflectArgs[i] = reflect.ValueOf(arg)
}
// Call function
results := f.Call(reflectArgs)
// Convert results back
output := make([]interface{}, len(results))
for i, result := range results {
output[i] = result.Interface()
}
return output
}
func main() {
result := callFunction(add, 5, 3)
fmt.Printf("Result: %v\n", result[0])
}
Creating Values Dynamically
Construct values at runtime.
Good: Dynamic Value Creation
package main
import (
"fmt"
"reflect"
)
func createValue(t reflect.Type) interface{} {
return reflect.New(t).Elem().Interface()
}
func main() {
// Create int
intVal := createValue(reflect.TypeOf(0))
fmt.Printf("Int: %v (type: %T)\n", intVal, intVal)
// Create string
strVal := createValue(reflect.TypeOf(""))
fmt.Printf("String: %v (type: %T)\n", strVal, strVal)
// Create slice
sliceVal := createValue(reflect.TypeOf([]int{}))
fmt.Printf("Slice: %v (type: %T)\n", sliceVal, sliceVal)
}
Reflection Performance
Reflection has significant overhead.
Good: Minimize Reflection
package main
import (
"fmt"
"reflect"
"time"
)
func withoutReflection(x int) int {
return x * 2
}
func withReflection(x interface{}) interface{} {
val := reflect.ValueOf(x)
return val.Interface()
}
func main() {
iterations := 1000000
// Without reflection
start := time.Now()
for i := 0; i < iterations; i++ {
_ = withoutReflection(42)
}
fmt.Printf("Without reflection: %v\n", time.Since(start))
// With reflection
start = time.Now()
for i := 0; i < iterations; i++ {
_ = withReflection(42)
}
fmt.Printf("With reflection: %v\n", time.Since(start))
}
Practical Reflection Example: JSON Marshaling
Good: Custom JSON Marshaling
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func marshalToJSON(v interface{}) (string, error) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
result := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
jsonTag = field.Name
}
result[jsonTag] = val.Field(i).Interface()
}
data, err := json.Marshal(result)
return string(data), err
}
func main() {
p := Person{Name: "Alice", Age: 30}
json, _ := marshalToJSON(p)
fmt.Println(json)
}
Reflection Pitfalls
Bad: Excessive Reflection
// โ AVOID: Using reflection for everything
package main
import (
"fmt"
"reflect"
)
func processAny(v interface{}) {
// Complex reflection logic
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if t.Kind() == reflect.Struct {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := val.Field(i)
fmt.Printf("%s: %v\n", field.Name, fieldVal.Interface())
}
}
}
func main() {
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
processAny(p)
}
Best Practices
- Avoid When Possible: Use type assertions first
- Cache Reflection Results: Don’t reflect repeatedly
- Handle Errors: Check for nil and invalid operations
- Document Usage: Explain why reflection is needed
- Test Thoroughly: Reflection errors occur at runtime
- Profile Impact: Measure reflection overhead
- Use Interfaces: Prefer interfaces over reflection
- Keep It Simple: Don’t over-engineer with reflection
Common Pitfalls
- Panic on Invalid Operations: Reflection can panic
- Performance Overhead: Reflection is slow
- Type Safety Loss: No compile-time checking
- Complexity: Hard to understand and maintain
- Debugging Difficulty: Stack traces are complex
Resources
Summary
Reflection enables runtime type inspection and manipulation but should be used sparingly. Prefer type assertions and interfaces when possible. When reflection is necessary, cache results, handle errors carefully, and be aware of performance implications. Use reflection for serialization, validation, and other meta-programming tasks where it provides clear value.
Comments