Skip to main content
โšก Calmops

Reflection in Go: Type Inspection

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

  1. Avoid When Possible: Use type assertions first
  2. Cache Reflection Results: Don’t reflect repeatedly
  3. Handle Errors: Check for nil and invalid operations
  4. Document Usage: Explain why reflection is needed
  5. Test Thoroughly: Reflection errors occur at runtime
  6. Profile Impact: Measure reflection overhead
  7. Use Interfaces: Prefer interfaces over reflection
  8. 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