Skip to content

🔬 Reflection

"Reflection is powerful, but comes with costs."
Go's reflect package cho phép runtime introspection, nhưng should be used sparingly.

📐 Reflection Basics

reflect.Type và reflect.Value

go
import "reflect"

func inspect(x interface{}) {
    t := reflect.TypeOf(x)   // Type information
    v := reflect.ValueOf(x)  // Value information
    
    fmt.Printf("Type: %s\n", t.Name())
    fmt.Printf("Kind: %s\n", t.Kind())
    fmt.Printf("Value: %v\n", v.Interface())
}

// Usage
inspect(42)
// Type: int
// Kind: int
// Value: 42

type User struct {
    Name string
    Age  int
}

inspect(User{Name: "Alice", Age: 30})
// Type: User
// Kind: struct
// Value: {Alice 30}

Kind vs Type

go
type UserID int64

var id UserID = 42

t := reflect.TypeOf(id)
fmt.Println(t.Name())  // UserID (concrete type)
fmt.Println(t.Kind())  // int64 (underlying kind)

// Kind enum includes:
// reflect.Int, reflect.String, reflect.Struct,
// reflect.Slice, reflect.Map, reflect.Ptr, etc.

🏗️ Struct Inspection

Field Iteration

go
type User struct {
    ID        int64  `json:"id" db:"id"`
    Name      string `json:"name" db:"name"`
    Email     string `json:"email" db:"email"`
    isAdmin   bool   // unexported
}

func inspectStruct(v interface{}) {
    t := reflect.TypeOf(v)
    val := reflect.ValueOf(v)
    
    // Handle pointer
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
        val = val.Elem()
    }
    
    if t.Kind() != reflect.Struct {
        fmt.Println("Not a struct")
        return
    }
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := val.Field(i)
        
        // Only exported fields are settable
        if field.PkgPath != "" {
            fmt.Printf("  %s: (unexported)\n", field.Name)
            continue
        }
        
        fmt.Printf("  %s (%s): %v\n", 
            field.Name, 
            field.Type, 
            value.Interface(),
        )
    }
}

user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
inspectStruct(user)
// ID (int64): 1
// Name (string): Alice
// Email (string): alice@example.com
// isAdmin: (unexported)

🏷️ Struct Tag Parsing

Reading Tags

go
type User struct {
    ID    int64  `json:"id" db:"user_id" validate:"required"`
    Name  string `json:"name" db:"user_name" validate:"required,min=2"`
    Email string `json:"email" db:"email" validate:"required,email"`
}

func getDBColumns(v interface{}) []string {
    t := reflect.TypeOf(v)
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }
    
    var columns []string
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        
        // Get "db" tag
        dbTag := field.Tag.Get("db")
        if dbTag != "" && dbTag != "-" {
            columns = append(columns, dbTag)
        }
    }
    return columns
}

columns := getDBColumns(User{})
// ["user_id", "user_name", "email"]

Custom Tag Parser

go
type FieldInfo struct {
    Name       string
    Column     string
    Required   bool
    Validation string
}

func parseStructTags(v interface{}) []FieldInfo {
    t := reflect.TypeOf(v)
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }
    
    var fields []FieldInfo
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        
        info := FieldInfo{
            Name:   field.Name,
            Column: field.Tag.Get("db"),
        }
        
        // Parse validate tag
        validate := field.Tag.Get("validate")
        if strings.Contains(validate, "required") {
            info.Required = true
        }
        info.Validation = validate
        
        fields = append(fields, info)
    }
    return fields
}

⚔️ Tradeoff: Reflection vs Alternatives

ApproachPerformanceType SafetyFlexibilityWhen to Use
ReflectionSlowNoneHighSerialization, ORM
Code GenFastCompile-timeMediumKnown types
GenericsFastCompile-timeMediumContainers
InterfaceFastCompile-timeLowKnown behavior
go
// Benchmark comparison
// reflect approach: ~500ns/op
// code gen approach: ~10ns/op (50x faster!)
// generics approach: ~15ns/op

🔧 Value Modification

Setting Values (Requires Pointer)

go
func setFieldByName(v interface{}, fieldName string, newValue interface{}) error {
    val := reflect.ValueOf(v)
    
    // Must be pointer to modify
    if val.Kind() != reflect.Ptr {
        return errors.New("must pass pointer")
    }
    
    val = val.Elem()  // Dereference
    
    field := val.FieldByName(fieldName)
    if !field.IsValid() {
        return fmt.Errorf("field %s not found", fieldName)
    }
    
    if !field.CanSet() {
        return fmt.Errorf("field %s cannot be set (unexported?)", fieldName)
    }
    
    newVal := reflect.ValueOf(newValue)
    if field.Type() != newVal.Type() {
        return fmt.Errorf("type mismatch: %s vs %s", field.Type(), newVal.Type())
    }
    
    field.Set(newVal)
    return nil
}

// Usage
user := &User{Name: "Alice"}
setFieldByName(user, "Name", "Bob")
fmt.Println(user.Name)  // Bob

🎭 Dynamic Function Calls

Method Invocation

go
type Calculator struct{}

func (c Calculator) Add(a, b int) int    { return a + b }
func (c Calculator) Multiply(a, b int) int { return a * b }

func callMethod(obj interface{}, method string, args ...interface{}) (interface{}, error) {
    val := reflect.ValueOf(obj)
    m := val.MethodByName(method)
    
    if !m.IsValid() {
        return nil, fmt.Errorf("method %s not found", method)
    }
    
    // Convert args to reflect.Value
    in := make([]reflect.Value, len(args))
    for i, arg := range args {
        in[i] = reflect.ValueOf(arg)
    }
    
    // Call method
    results := m.Call(in)
    
    if len(results) == 0 {
        return nil, nil
    }
    return results[0].Interface(), nil
}

// Usage
calc := Calculator{}
result, _ := callMethod(calc, "Add", 5, 3)
fmt.Println(result)  // 8

💻 Engineering Example: Generic JSON Validator

go
package validator

import (
    "fmt"
    "reflect"
    "regexp"
    "strconv"
    "strings"
)

type ValidationError struct {
    Field   string
    Message string
}

func Validate(v interface{}) []ValidationError {
    var errors []ValidationError
    
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    
    t := val.Type()
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fieldVal := val.Field(i)
        
        rules := field.Tag.Get("validate")
        if rules == "" {
            continue
        }
        
        for _, rule := range strings.Split(rules, ",") {
            if err := validateRule(field.Name, fieldVal, rule); err != nil {
                errors = append(errors, *err)
            }
        }
    }
    
    return errors
}

func validateRule(fieldName string, val reflect.Value, rule string) *ValidationError {
    parts := strings.SplitN(rule, "=", 2)
    ruleName := parts[0]
    ruleArg := ""
    if len(parts) > 1 {
        ruleArg = parts[1]
    }
    
    switch ruleName {
    case "required":
        if isZero(val) {
            return &ValidationError{fieldName, "is required"}
        }
        
    case "min":
        min, _ := strconv.Atoi(ruleArg)
        switch val.Kind() {
        case reflect.String:
            if val.Len() < min {
                return &ValidationError{fieldName, fmt.Sprintf("must be at least %d characters", min)}
            }
        case reflect.Int, reflect.Int64:
            if val.Int() < int64(min) {
                return &ValidationError{fieldName, fmt.Sprintf("must be at least %d", min)}
            }
        }
        
    case "max":
        max, _ := strconv.Atoi(ruleArg)
        switch val.Kind() {
        case reflect.String:
            if val.Len() > max {
                return &ValidationError{fieldName, fmt.Sprintf("must be at most %d characters", max)}
            }
        }
        
    case "email":
        if val.Kind() == reflect.String {
            emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
            if !emailRegex.MatchString(val.String()) {
                return &ValidationError{fieldName, "must be a valid email"}
            }
        }
    }
    
    return nil
}

func isZero(v reflect.Value) bool {
    return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}

// Usage
type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=18"`
}

func main() {
    req := CreateUserRequest{
        Name:  "A",
        Email: "invalid",
        Age:   15,
    }
    
    errors := Validate(req)
    for _, e := range errors {
        fmt.Printf("%s: %s\n", e.Field, e.Message)
    }
    // Name: must be at least 2 characters
    // Email: must be a valid email
    // Age: must be at least 18
}

Ship-to-Prod Checklist

Performance

  • [ ] Cache reflect.Type để tránh repeated lookups
  • [ ] Benchmark reflection code paths
  • [ ] Consider code generation cho hot paths
  • [ ] Avoid reflection trong tight loops

Safety

  • [ ] Handle panics từ invalid operations
  • [ ] Check CanSet() trước khi modify
  • [ ] Validate Kind trước khi assume type
  • [ ] Handle nil values explicitly

Code Quality

  • [ ] Document why reflection is needed
  • [ ] Limit scope - isolate reflection code
  • [ ] Test edge cases (nil, zero values, unexported)
  • [ ] Consider alternatives (generics, interfaces, code gen)

📊 Summary

OperationFunctionNote
Get Typereflect.TypeOf(x)Returns reflect.Type
Get Valuereflect.ValueOf(x)Returns reflect.Value
Get Kindt.Kind()Underlying type category
Get Fieldt.Field(i)Struct field by index
Get Tagfield.Tag.Get("json")Tag value
Set Valuev.Set(newVal)Requires pointer
Call Methodv.MethodByName("Foo").Call(args)Dynamic dispatch

➡️ Tiếp theo

Reflection nắm vững rồi! Tiếp theo: CGO & FFI - Calling C code from Go.