Working with Go’s reflection capabilities has transformed how I approach dynamic programming challenges. After years of building systems that require runtime introspection, I’ve discovered seven essential techniques that make reflection both powerful and practical.
Understanding Struct Tag Parsing
Struct tags provide metadata that drives application behavior at runtime. I’ve used this technique extensively in validation frameworks, ORM systems, and configuration parsers. The key lies in extracting meaningful information from tags and using it to control program flow.
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Product struct {
ID int `json:"id" validate:"required,min=1"`
Name string `json:"name" validate:"required,min=3,max=100"`
Price float64 `json:"price" validate:"required,min=0.01"`
Category string `json:"category" validate:"required"`
}
func parseValidationRules(tag string) map[string]string {
rules := make(map[string]string)
parts := strings.Split(tag, ",")
for _, part := range parts {
if strings.Contains(part, "=") {
kv := strings.SplitN(part, "=", 2)
rules[kv[0]] = kv[1]
} else {
rules[part] = ""
}
}
return rules
}
func validateField(value reflect.Value, rules map[string]string) error {
if _, exists := rules["required"]; exists {
if value.IsZero() {
return fmt.Errorf("field is required")
}
}
switch value.Kind() {
case reflect.String:
str := value.String()
if minStr, exists := rules["min"]; exists {
if min, err := strconv.Atoi(minStr); err == nil && len(str) < min {
return fmt.Errorf("string too short, minimum length: %d", min)
}
}
if maxStr, exists := rules["max"]; exists {
if max, err := strconv.Atoi(maxStr); err == nil && len(str) > max {
return fmt.Errorf("string too long, maximum length: %d", max)
}
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
num := value.Int()
if minStr, exists := rules["min"]; exists {
if min, err := strconv.ParseInt(minStr, 10, 64); err == nil && num < min {
return fmt.Errorf("value too small, minimum: %d", min)
}
}
case reflect.Float32, reflect.Float64:
num := value.Float()
if minStr, exists := rules["min"]; exists {
if min, err := strconv.ParseFloat(minStr, 64); err == nil && num < min {
return fmt.Errorf("value too small, minimum: %f", min)
}
}
}
return nil
}
func validateStruct(s interface{}) []error {
var errors []error
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
if tag := field.Tag.Get("validate"); tag != "" {
rules := parseValidationRules(tag)
if err := validateField(value, rules); err != nil {
errors = append(errors, fmt.Errorf("field %s: %w", field.Name, err))
}
}
}
return errors
}
This validation system demonstrates how struct tags create declarative behavior. The tags define validation rules without cluttering the struct with validation logic.
Dynamic Method Invocation
Method invocation at runtime enables plugin architectures and dynamic dispatch systems. I’ve built several systems where method names come from configuration files or user input, requiring runtime method resolution.
package main
import (
"fmt"
"reflect"
)
type Calculator struct {
History []string
}
func (c *Calculator) Add(a, b float64) float64 {
result := a + b
c.History = append(c.History, fmt.Sprintf("Add(%.2f, %.2f) = %.2f", a, b, result))
return result
}
func (c *Calculator) Multiply(a, b float64) float64 {
result := a * b
c.History = append(c.History, fmt.Sprintf("Multiply(%.2f, %.2f) = %.2f", a, b, result))
return result
}
func (c *Calculator) Subtract(a, b float64) float64 {
result := a - b
c.History = append(c.History, fmt.Sprintf("Subtract(%.2f, %.2f) = %.2f", a, b, result))
return result
}
func (c *Calculator) GetHistory() []string {
return c.History
}
type MethodInvoker struct {
target reflect.Value
}
func NewMethodInvoker(target interface{}) *MethodInvoker {
return &MethodInvoker{
target: reflect.ValueOf(target),
}
}
func (mi *MethodInvoker) CallMethod(methodName string, args ...interface{}) ([]reflect.Value, error) {
method := mi.target.MethodByName(methodName)
if !method.IsValid() {
return nil, fmt.Errorf("method %s not found", methodName)
}
methodType := method.Type()
if len(args) != methodType.NumIn() {
return nil, fmt.Errorf("expected %d arguments, got %d", methodType.NumIn(), len(args))
}
values := make([]reflect.Value, len(args))
for i, arg := range args {
argValue := reflect.ValueOf(arg)
expectedType := methodType.In(i)
if !argValue.Type().AssignableTo(expectedType) {
return nil, fmt.Errorf("argument %d: cannot assign %s to %s",
i, argValue.Type(), expectedType)
}
values[i] = argValue
}
results := method.Call(values)
return results, nil
}
func (mi *MethodInvoker) ListMethods() []string {
var methods []string
t := mi.target.Type()
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
methods = append(methods, method.Name)
}
return methods
}
Dynamic method invocation requires careful type checking and error handling. The invoker validates argument types before calling methods, preventing runtime panics.
Type Switching with Reflection
Runtime type identification enables generic algorithms that adapt to different types. I’ve used this technique in serialization libraries and data processing pipelines where the exact type isn’t known until runtime.
package main
import (
"fmt"
"reflect"
"time"
)
type DataProcessor struct {
processors map[reflect.Type]func(reflect.Value) string
}
func NewDataProcessor() *DataProcessor {
return &DataProcessor{
processors: make(map[reflect.Type]func(reflect.Value) string),
}
}
func (dp *DataProcessor) RegisterProcessor(t reflect.Type, processor func(reflect.Value) string) {
dp.processors[t] = processor
}
func (dp *DataProcessor) Process(data interface{}) string {
value := reflect.ValueOf(data)
typ := value.Type()
// Handle pointers by getting the underlying type
if typ.Kind() == reflect.Ptr {
if value.IsNil() {
return "nil"
}
value = value.Elem()
typ = value.Type()
}
// Check for registered processor
if processor, exists := dp.processors[typ]; exists {
return processor(value)
}
// Default processing based on kind
return dp.processDefault(value)
}
func (dp *DataProcessor) processDefault(value reflect.Value) string {
switch value.Kind() {
case reflect.String:
return fmt.Sprintf("String: %q", value.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("Integer: %d", value.Int())
case reflect.Float32, reflect.Float64:
return fmt.Sprintf("Float: %.2f", value.Float())
case reflect.Bool:
return fmt.Sprintf("Boolean: %t", value.Bool())
case reflect.Slice:
return dp.processSlice(value)
case reflect.Map:
return dp.processMap(value)
case reflect.Struct:
return dp.processStruct(value)
default:
return fmt.Sprintf("Unknown type: %s", value.Type())
}
}
func (dp *DataProcessor) processSlice(value reflect.Value) string {
result := "Slice["
for i := 0; i < value.Len(); i++ {
if i > 0 {
result += ", "
}
result += dp.processDefault(value.Index(i))
}
result += "]"
return result
}
func (dp *DataProcessor) processMap(value reflect.Value) string {
result := "Map{"
keys := value.MapKeys()
for i, key := range keys {
if i > 0 {
result += ", "
}
mapValue := value.MapIndex(key)
result += fmt.Sprintf("%s: %s",
dp.processDefault(key),
dp.processDefault(mapValue))
}
result += "}"
return result
}
func (dp *DataProcessor) processStruct(value reflect.Value) string {
typ := value.Type()
result := fmt.Sprintf("%s{", typ.Name())
for i := 0; i < value.NumField(); i++ {
if i > 0 {
result += ", "
}
field := typ.Field(i)
fieldValue := value.Field(i)
result += fmt.Sprintf("%s: %s", field.Name, dp.processDefault(fieldValue))
}
result += "}"
return result
}
// Example usage with custom processors
func setupCustomProcessors() *DataProcessor {
dp := NewDataProcessor()
// Custom processor for time.Time
dp.RegisterProcessor(reflect.TypeOf(time.Time{}), func(v reflect.Value) string {
t := v.Interface().(time.Time)
return fmt.Sprintf("Time: %s", t.Format("2006-01-02 15:04:05"))
})
return dp
}
This type switching system provides extensibility through custom processors while maintaining safe defaults for common types.
Interface Conversion Checking
Safe interface conversions prevent runtime panics when working with unknown types. I’ve implemented this pattern in plugin systems where external code might not implement expected interfaces correctly.
package main
import (
"fmt"
"reflect"
)
type Serializer interface {
Serialize() ([]byte, error)
}
type Validator interface {
Validate() error
}
type Processor interface {
Process() error
}
type InterfaceChecker struct {
serializerType reflect.Type
validatorType reflect.Type
processorType reflect.Type
}
func NewInterfaceChecker() *InterfaceChecker {
return &InterfaceChecker{
serializerType: reflect.TypeOf((*Serializer)(nil)).Elem(),
validatorType: reflect.TypeOf((*Validator)(nil)).Elem(),
processorType: reflect.TypeOf((*Processor)(nil)).Elem(),
}
}
func (ic *InterfaceChecker) CheckCapabilities(obj interface{}) map[string]bool {
capabilities := make(map[string]bool)
objType := reflect.TypeOf(obj)
capabilities["Serializer"] = objType.Implements(ic.serializerType)
capabilities["Validator"] = objType.Implements(ic.validatorType)
capabilities["Processor"] = objType.Implements(ic.processorType)
return capabilities
}
func (ic *InterfaceChecker) SafeCast(obj interface{}, targetInterface string) (interface{}, error) {
objType := reflect.TypeOf(obj)
switch targetInterface {
case "Serializer":
if objType.Implements(ic.serializerType) {
return obj.(Serializer), nil
}
return nil, fmt.Errorf("object does not implement Serializer interface")
case "Validator":
if objType.Implements(ic.validatorType) {
return obj.(Validator), nil
}
return nil, fmt.Errorf("object does not implement Validator interface")
case "Processor":
if objType.Implements(ic.processorType) {
return obj.(Processor), nil
}
return nil, fmt.Errorf("object does not implement Processor interface")
default:
return nil, fmt.Errorf("unknown interface: %s", targetInterface)
}
}
// Example implementations
type Document struct {
Title string
Content string
}
func (d *Document) Serialize() ([]byte, error) {
return []byte(fmt.Sprintf("Title: %s\nContent: %s", d.Title, d.Content)), nil
}
func (d *Document) Validate() error {
if d.Title == "" {
return fmt.Errorf("title is required")
}
if d.Content == "" {
return fmt.Errorf("content is required")
}
return nil
}
type SimpleData struct {
Value string
}
func (sd *SimpleData) Process() error {
fmt.Printf("Processing: %s\n", sd.Value)
return nil
}
func demonstrateInterfaceChecking() {
checker := NewInterfaceChecker()
doc := &Document{Title: "Test", Content: "Sample content"}
simple := &SimpleData{Value: "test data"}
fmt.Println("Document capabilities:", checker.CheckCapabilities(doc))
fmt.Println("SimpleData capabilities:", checker.CheckCapabilities(simple))
// Safe casting
if serializer, err := checker.SafeCast(doc, "Serializer"); err == nil {
data, _ := serializer.(Serializer).Serialize()
fmt.Printf("Serialized: %s\n", string(data))
}
if processor, err := checker.SafeCast(simple, "Processor"); err == nil {
processor.(Processor).Process()
}
// This would fail safely
if _, err := checker.SafeCast(simple, "Serializer"); err != nil {
fmt.Printf("Expected error: %s\n", err)
}
}
Interface checking provides runtime safety by verifying interface compliance before attempting conversions.
Field Manipulation for Generic Processing
Generic field manipulation enables data transformation libraries that work across different struct types. I’ve used this technique in ORM systems and data migration tools.
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
type FieldProcessor struct {
transformers map[reflect.Type]func(reflect.Value) reflect.Value
}
func NewFieldProcessor() *FieldProcessor {
fp := &FieldProcessor{
transformers: make(map[reflect.Type]func(reflect.Value) reflect.Value),
}
// Register default transformers
fp.RegisterTransformer(reflect.TypeOf(""), fp.stringTransformer)
fp.RegisterTransformer(reflect.TypeOf(time.Time{}), fp.timeTransformer)
return fp
}
func (fp *FieldProcessor) RegisterTransformer(t reflect.Type, transformer func(reflect.Value) reflect.Value) {
fp.transformers[t] = transformer
}
func (fp *FieldProcessor) stringTransformer(value reflect.Value) reflect.Value {
str := strings.TrimSpace(value.String())
return reflect.ValueOf(str)
}
func (fp *FieldProcessor) timeTransformer(value reflect.Value) reflect.Value {
// Normalize time to UTC
t := value.Interface().(time.Time)
return reflect.ValueOf(t.UTC())
}
func (fp *FieldProcessor) ProcessStruct(obj interface{}) interface{} {
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// Create a new instance of the same type
newValue := reflect.New(value.Type()).Elem()
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
fieldType := field.Type()
var processedField reflect.Value
if transformer, exists := fp.transformers[fieldType]; exists {
processedField = transformer(field)
} else {
processedField = field
}
if newValue.Field(i).CanSet() {
newValue.Field(i).Set(processedField)
}
}
return newValue.Interface()
}
func (fp *FieldProcessor) CopyFields(src, dst interface{}, fieldMap map[string]string) error {
srcValue := reflect.ValueOf(src)
dstValue := reflect.ValueOf(dst)
if srcValue.Kind() == reflect.Ptr {
srcValue = srcValue.Elem()
}
if dstValue.Kind() == reflect.Ptr {
dstValue = dstValue.Elem()
}
srcType := srcValue.Type()
dstType := dstValue.Type()
for srcFieldName, dstFieldName := range fieldMap {
srcField, found := srcType.FieldByName(srcFieldName)
if !found {
return fmt.Errorf("source field %s not found", srcFieldName)
}
dstField, found := dstType.FieldByName(dstFieldName)
if !found {
return fmt.Errorf("destination field %s not found", dstFieldName)
}
srcValue := srcValue.FieldByName(srcFieldName)
dstValue := dstValue.FieldByName(dstFieldName)
if !dstValue.CanSet() {
return fmt.Errorf("destination field %s cannot be set", dstFieldName)
}
// Type compatibility check
if !srcValue.Type().AssignableTo(dstField.Type) {
return fmt.Errorf("cannot assign %s to %s", srcValue.Type(), dstField.Type)
}
dstValue.Set(srcValue)
}
return nil
}
func (fp *FieldProcessor) GetFieldInfo(obj interface{}) []FieldInfo {
var fields []FieldInfo
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
typ := value.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldValue := value.Field(i)
info := FieldInfo{
Name: field.Name,
Type: field.Type.String(),
Tag: string(field.Tag),
Value: fmt.Sprintf("%v", fieldValue.Interface()),
CanSet: fieldValue.CanSet(),
IsZero: fieldValue.IsZero(),
IsExported: field.PkgPath == "",
}
fields = append(fields, info)
}
return fields
}
type FieldInfo struct {
Name string
Type string
Tag string
Value string
CanSet bool
IsZero bool
IsExported bool
}
// Example usage structures
type SourceData struct {
ID int `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
Email string `json:"email"`
}
type TargetData struct {
UserID int `json:"user_id"`
FullName string `json:"full_name"`
Timestamp time.Time `json:"timestamp"`
}
Field manipulation provides the foundation for building generic data processing systems that adapt to different struct types.
Function Signature Inspection
Function signature analysis enables middleware systems and dynamic call routing. I’ve built several HTTP routers and RPC systems using this technique to automatically handle parameter binding and response formatting.
package main
import (
"fmt"
"reflect"
)
type FunctionInspector struct {
registry map[string]FunctionInfo
}
type FunctionInfo struct {
Name string
Function reflect.Value
Type reflect.Type
Parameters []ParameterInfo
Returns []ReturnInfo
}
type ParameterInfo struct {
Name string
Type reflect.Type
}
type ReturnInfo struct {
Type reflect.Type
}
func NewFunctionInspector() *FunctionInspector {
return &FunctionInspector{
registry: make(map[string]FunctionInfo),
}
}
func (fi *FunctionInspector) RegisterFunction(name string, fn interface{}) error {
fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()
if fnType.Kind() != reflect.Func {
return fmt.Errorf("provided value is not a function")
}
info := FunctionInfo{
Name: name,
Function: fnValue,
Type: fnType,
}
// Extract parameter information
for i := 0; i < fnType.NumIn(); i++ {
paramType := fnType.In(i)
info.Parameters = append(info.Parameters, ParameterInfo{
Type: paramType,
})
}
// Extract return information
for i := 0; i < fnType.NumOut(); i++ {
returnType := fnType.Out(i)
info.Returns = append(info.Returns, ReturnInfo{
Type: returnType,
})
}
fi.registry[name] = info
return nil
}
func (fi *FunctionInspector) CallFunction(name string, args ...interface{}) ([]interface{}, error) {
info, exists := fi.registry[name]
if !exists {
return nil, fmt.Errorf("function %s not found", name)
}
if len(args) != len(info.Parameters) {
return nil, fmt.Errorf("expected %d arguments, got %d", len(info.Parameters), len(args))
}
// Prepare arguments
callArgs := make([]reflect.Value, len(args))
for i, arg := range args {
argValue := reflect.ValueOf(arg)
expectedType := info.Parameters[i].Type
if !argValue.Type().AssignableTo(expectedType) {
return nil, fmt.Errorf("argument %d: cannot assign %s to %s",
i, argValue.Type(), expectedType)
}
callArgs[i] = argValue
}
// Call function
results := info.Function.Call(callArgs)
// Convert results to interface{}
returnValues := make([]interface{}, len(results))
for i, result := range results {
returnValues[i] = result.Interface()
}
return returnValues, nil
}
func (fi *FunctionInspector) GetSignature(name string) (string, error) {
info, exists := fi.registry[name]
if !exists {
return "", fmt.Errorf("function %s not found", name)
}
signature := fmt.Sprintf("%s(", name)
for i, param := range info.Parameters {
if i > 0 {
signature += ", "
}
signature += param.Type.String()
}
signature += ")"
if len(info.Returns) > 0 {
signature += " ("
for i, ret := range info.Returns {
if i > 0 {
signature += ", "
}
signature += ret.Type.String()
}
signature += ")"
}
return signature, nil
}
func (fi *FunctionInspector) ListFunctions() []string {
var names []string
for name := range fi.registry {
names = append(names, name)
}
return names
}
// Example functions to register
func AddNumbers(a, b int) int {
return a + b
}
func FormatString(template string, args ...interface{}) string {
return fmt.Sprintf(template, args...)
}
func ProcessData(data map[string]interface{}) (bool, error) {
if len(data) == 0 {
return false, fmt.Errorf("empty data")
}
return true, nil
}
func demonstrateFunctionInspection() {
inspector := NewFunctionInspector()
// Register functions
inspector.RegisterFunction("add", AddNumbers)
inspector.RegisterFunction("format", FormatString)
inspector.RegisterFunction("process", ProcessData)
// List all functions
fmt.Println("Registered functions:")
for _, name := range inspector.ListFunctions() {
signature, _ := inspector.GetSignature(name)
fmt.Printf(" %s\n", signature)
}
// Call functions dynamically
if result, err := inspector.CallFunction("add", 5, 3); err == nil {
fmt.Printf("add(5, 3) = %v\n", result[0])
}
if result, err := inspector.CallFunction("format", "Hello %s!", "World"); err == nil {
fmt.Printf("format result: %v\n", result[0])
}
data := map[string]interface{}{"key": "value"}
if result, err := inspector.CallFunction("process", data); err == nil {
fmt.Printf("process result: success=%v, error=%v\n", result[0], result[1])
}
}
Function signature inspection creates flexible systems that can work with arbitrary functions while maintaining type safety.
Performance Optimization Strategies
Reflection performance requires careful consideration. I’ve learned to cache reflection objects, avoid reflection in hot paths, and use code generation when performance is critical.
package main
import (
"reflect"
"sync"
"time"
)
type ReflectionCache struct {
types sync.Map
values sync.Map
fields sync.Map
}
type CachedType struct {
Type reflect.Type
Fields []reflect.StructField
Methods []reflect.Method
CachedAt time.Time
}
type CachedValue struct {
Value reflect.Value
CachedAt time.Time
}
func NewReflectionCache() *ReflectionCache {
return &ReflectionCache{}
}
func (c *ReflectionCache) GetType(obj interface{}) *CachedType {
key := reflect.TypeOf(obj).String()
if cached, ok := c.types.Load(key); ok {
return cached.(*CachedType)
}
typ := reflect.TypeOf(obj)
cached := &CachedType{
Type: typ,
CachedAt: time.Now(),
}
// Cache struct fields
if typ.Kind() == reflect.Struct {
for i := 0; i < typ.NumField(); i++ {
cached.Fields = append(cached.Fields, typ.Field(i))
}
}
// Cache methods
for i := 0; i < typ.NumMethod(); i++ {
cached.Methods = append(cached.Methods, typ.Method(i))
}
c.types.Store(key, cached)
return cached
}
func (c *ReflectionCache) GetValue(obj interface{}) reflect.Value {
// For demonstration - in practice, you'd need a more sophisticated key
value := reflect.ValueOf(obj)
return value
}
// Optimized struct processor using caching
type OptimizedProcessor struct {
cache *ReflectionCache
processors map[string]func(interface{}) interface{}
}
func NewOptimizedProcessor() *OptimizedProcessor {
return &OptimizedProcessor{
cache: NewReflectionCache(),
processors: make(map[string]func(interface{}) interface{}),
}
}
func (op *OptimizedProcessor) ProcessStruct(obj interface{}) interface{} {
cached := op.cache.GetType(obj)
// Use cached type information instead of repeated reflection calls
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
result := make(map[string]interface{})
for _, field := range cached.Fields {
fieldValue := value.FieldByIndex(field.Index)
result[field.Name] = fieldValue.Interface()
}
return result
}
// Benchmark comparison structures
type BenchmarkData struct {
ID int
Name string
Email string
Active bool
Score float64
Tags []string
Metadata map[string]string
}
func processWithoutCache(obj interface{}) map[string]interface{} {
result := make(map[string]interface{})
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
typ := value.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldValue := value.Field(i)
result[field.Name] = fieldValue.Interface()
}
return result
}
func processWithCache(obj interface{}, cache *ReflectionCache) map[string]interface{} {
cached := cache.GetType(obj)
result := make(map[string]interface{})
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
for _, field := range cached.Fields {
fieldValue := value.FieldByIndex(field.Index)
result[field.Name] = fieldValue.Interface()
}
return result
}
// Code generation alternative
func generateStructProcessor(typeName string) string {
return fmt.Sprintf(`
func Process%s(obj *%s) map[string]interface{} {
return map[string