Advanced Reflection Techniques
With a solid understanding of reflection fundamentals, we can now explore more sophisticated techniques that enable powerful metaprogramming capabilities in Go. These advanced patterns are what power many of Go’s most flexible libraries and frameworks.
Dynamic Method Invocation
One of reflection’s most powerful capabilities is the ability to call methods dynamically at runtime:
package main
import (
"fmt"
"reflect"
)
type Calculator struct {
result float64
}
func (c *Calculator) Add(x float64) {
c.result += x
}
func (c *Calculator) Subtract(x float64) {
c.result -= x
}
func (c *Calculator) Multiply(x float64) {
c.result *= x
}
func (c *Calculator) Divide(x float64) {
c.result /= x
}
func (c *Calculator) Result() float64 {
return c.result
}
// ExecuteOperation dynamically calls a method on the calculator
func ExecuteOperation(calc *Calculator, operation string, value float64) error {
// Get the reflect.Value of the calculator pointer
v := reflect.ValueOf(calc)
// Find the method by name
method := v.MethodByName(operation)
if !method.IsValid() {
return fmt.Errorf("method not found: %s", operation)
}
// Check if the method takes one float64 argument
methodType := method.Type()
if methodType.NumIn() != 1 || methodType.In(0).Kind() != reflect.Float64 {
return fmt.Errorf("method %s does not take a single float64 argument", operation)
}
// Call the method with the value
args := []reflect.Value{reflect.ValueOf(value)}
method.Call(args)
return nil
}
func main() {
calc := &Calculator{result: 10}
// Execute a sequence of operations dynamically
operations := []struct {
name string
value float64
}{
{"Add", 5}, // 10 + 5 = 15
{"Multiply", 2}, // 15 * 2 = 30
{"Subtract", 8}, // 30 - 8 = 22
{"Divide", 2}, // 22 / 2 = 11
}
for _, op := range operations {
if err := ExecuteOperation(calc, op.name, op.value); err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("After %s(%.1f): %.1f\n", op.name, op.value, calc.Result())
}
}
Output:
After Add(5.0): 15.0
After Multiply(2.0): 30.0
After Subtract(8.0): 22.0
After Divide(2.0): 11.0
This pattern enables plugin systems, command dispatchers, and other architectures where the exact methods to call aren’t known until runtime.
Deep Struct Copying and Cloning
Reflection enables deep copying of complex nested structures:
package main
import (
"fmt"
"reflect"
)
// DeepCopy creates a deep copy of any value
func DeepCopy(v interface{}) interface{} {
// Handle nil
if v == nil {
return nil
}
// Get reflect.Value of the input
original := reflect.ValueOf(v)
// Handle simple types that can be copied directly
switch original.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String:
return original.Interface()
}
// Create a new value of the same type as the original
copy := reflect.New(original.Type()).Elem()
// Handle different complex types
switch original.Kind() {
case reflect.Ptr:
// If it's nil, return nil
if original.IsNil() {
return nil
}
// Create a deep copy of what the pointer points to
originalValue := original.Elem().Interface()
copyValue := DeepCopy(originalValue)
// Create a new pointer to the copied value
copyPtr := reflect.New(reflect.TypeOf(copyValue))
copyPtr.Elem().Set(reflect.ValueOf(copyValue))
return copyPtr.Interface()
case reflect.Struct:
// Copy each field
for i := 0; i < original.NumField(); i++ {
field := original.Field(i)
if field.CanInterface() { // Skip unexported fields
fieldCopy := DeepCopy(field.Interface())
copy.Field(i).Set(reflect.ValueOf(fieldCopy))
}
}
return copy.Interface()
case reflect.Slice:
// Handle nil slice
if original.IsNil() {
return reflect.Zero(original.Type()).Interface()
}
// Create a new slice with the same length and capacity
copySlice := reflect.MakeSlice(original.Type(), original.Len(), original.Cap())
// Copy each element
for i := 0; i < original.Len(); i++ {
elem := original.Index(i).Interface()
copyElem := DeepCopy(elem)
copySlice.Index(i).Set(reflect.ValueOf(copyElem))
}
return copySlice.Interface()
case reflect.Map:
// Handle nil map
if original.IsNil() {
return reflect.Zero(original.Type()).Interface()
}
// Create a new map
copyMap := reflect.MakeMap(original.Type())
// Copy each key-value pair
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key).Interface()
copyKey := DeepCopy(key.Interface())
copyValue := DeepCopy(originalValue)
copyMap.SetMapIndex(reflect.ValueOf(copyKey), reflect.ValueOf(copyValue))
}
return copyMap.Interface()
default:
// For other types, return as is (may not be a deep copy)
return original.Interface()
}
}
type Address struct {
Street string
City string
}
type Person struct {
Name string
Age int
Address *Address
Roles []string
Data map[string]interface{}
}
func main() {
// Create a complex nested structure
original := Person{
Name: "Alice",
Age: 30,
Address: &Address{
Street: "123 Main St",
City: "Wonderland",
},
Roles: []string{"Admin", "User"},
Data: map[string]interface{}{
"id": 12345,
"active": true,
"profile": &Address{Street: "Work Address", City: "Office City"},
},
}
// Create a deep copy
copy := DeepCopy(original).(Person)
// Modify the original to demonstrate the copy is independent
original.Name = "Modified Alice"
original.Address.Street = "456 Changed St"
original.Roles[0] = "Modified Admin"
original.Data["active"] = false
// Print both to compare
fmt.Println("Original:")
fmt.Printf(" Name: %s\n", original.Name)
fmt.Printf(" Address: %s, %s\n", original.Address.Street, original.Address.City)
fmt.Printf(" Roles: %v\n", original.Roles)
fmt.Printf(" Active: %v\n", original.Data["active"])
fmt.Println("\nCopy:")
fmt.Printf(" Name: %s\n", copy.Name)
fmt.Printf(" Address: %s, %s\n", copy.Address.Street, copy.Address.City)
fmt.Printf(" Roles: %v\n", copy.Roles)
fmt.Printf(" Active: %v\n", copy.Data["active"])
}
Output:
Original:
Name: Modified Alice
Address: 456 Changed St, Wonderland
Roles: [Modified Admin User]
Active: false
Copy:
Name: Alice
Address: 123 Main St, Wonderland
Roles: [Admin User]
Active: true
This deep copying technique is valuable for creating independent copies of complex data structures, which is essential in concurrent applications or when working with immutable data patterns.
Advanced Struct Tag Processing
Struct tags are a powerful feature in Go that enable metadata-driven programming. Advanced reflection techniques can extract and process this metadata in sophisticated ways:
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// Validator defines validation rules for struct fields
type Validator struct {
rules map[string][]ValidationRule
}
// ValidationRule represents a single validation rule
type ValidationRule struct {
Name string
Param string
}
// ValidationError represents a validation error
type ValidationError struct {
Field string
Rule string
Msg string
}
// NewValidator creates a new validator
func NewValidator() *Validator {
return &Validator{
rules: make(map[string][]ValidationRule),
}
}
// ParseValidationRules extracts validation rules from struct tags
func (v *Validator) ParseValidationRules(s interface{}) {
t := reflect.TypeOf(s)
// If pointer, get the underlying type
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// Only process structs
if t.Kind() != reflect.Struct {
return
}
// Iterate through all fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Get the validate tag
validateTag := field.Tag.Get("validate")
if validateTag == "" {
continue
}
// Parse the validation rules
fieldRules := []ValidationRule{}
rules := strings.Split(validateTag, ",")
for _, rule := range rules {
// Split rule into name and parameter
parts := strings.SplitN(rule, "=", 2)
name := parts[0]
param := ""
if len(parts) > 1 {
param = parts[1]
}
fieldRules = append(fieldRules, ValidationRule{
Name: name,
Param: param,
})
}
v.rules[field.Name] = fieldRules
}
}
// Validate validates a struct against the parsed rules
func (v *Validator) Validate(s interface{}) []ValidationError {
errors := []ValidationError{}
val := reflect.ValueOf(s)
// If pointer, get the underlying value
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// Only process structs
if val.Kind() != reflect.Struct {
return errors
}
// Validate each field
for fieldName, rules := range v.rules {
field := val.FieldByName(fieldName)
// Skip if field doesn't exist
if !field.IsValid() {
continue
}
// Apply each rule
for _, rule := range rules {
var err *ValidationError
switch rule.Name {
case "required":
if isZero(field) {
err = &ValidationError{
Field: fieldName,
Rule: "required",
Msg: fmt.Sprintf("%s is required", fieldName),
}
}
case "min":
min, _ := strconv.Atoi(rule.Param)
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if field.Int() < int64(min) {
err = &ValidationError{
Field: fieldName,
Rule: "min",
Msg: fmt.Sprintf("%s must be at least %d", fieldName, min),
}
}
case reflect.String:
if field.Len() < min {
err = &ValidationError{
Field: fieldName,
Rule: "min",
Msg: fmt.Sprintf("%s must be at least %d characters", fieldName, min),
}
}
}
case "max":
max, _ := strconv.Atoi(rule.Param)
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if field.Int() > int64(max) {
err = &ValidationError{
Field: fieldName,
Rule: "max",
Msg: fmt.Sprintf("%s must be at most %d", fieldName, max),
}
}
case reflect.String:
if field.Len() > max {
err = &ValidationError{
Field: fieldName,
Rule: "max",
Msg: fmt.Sprintf("%s must be at most %d characters", fieldName, max),
}
}
}
}
if err != nil {
errors = append(errors, *err)
}
}
}
return errors
}
// isZero checks if a value is the zero value for its type
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Ptr, reflect.Interface:
return v.IsNil()
}
return false
}
type User struct {
Username string `validate:"required,min=3,max=20"`
Password string `validate:"required,min=8"`
Email string `validate:"required"`
Age int `validate:"min=18,max=120"`
}
func main() {
// Create a validator and parse rules
validator := NewValidator()
validator.ParseValidationRules(User{})
// Test with valid user
validUser := User{
Username: "johndoe",
Password: "securepass123",
Email: "[email protected]",
Age: 30,
}
errors := validator.Validate(validUser)
fmt.Printf("Valid user has %d validation errors\n", len(errors))
// Test with invalid user
invalidUser := User{
Username: "jo",
Password: "weak",
Email: "",
Age: 15,
}
errors = validator.Validate(invalidUser)
fmt.Printf("Invalid user has %d validation errors:\n", len(errors))
for _, err := range errors {
fmt.Printf(" - %s\n", err.Msg)
}
}
Output:
Valid user has 0 validation errors
Invalid user has 4 validation errors:
- Username must be at least 3 characters
- Password must be at least 8 characters
- Email is required
- Age must be at least 18
This validation framework demonstrates how reflection can be used to build flexible, metadata-driven systems. Similar approaches power many popular Go libraries like encoding/json
, gorm
, and validation frameworks.
Dynamic Struct Creation
Reflection allows for creating new struct types at runtime, which is particularly useful for data mapping scenarios:
package main
import (
"fmt"
"reflect"
)
// DynamicStruct allows building struct types at runtime
type DynamicStruct struct {
fields []reflect.StructField
fieldNames map[string]bool
}
// NewDynamicStruct creates a new dynamic struct builder
func NewDynamicStruct() *DynamicStruct {
return &DynamicStruct{
fields: make([]reflect.StructField, 0),
fieldNames: make(map[string]bool),
}
}
// AddField adds a field to the dynamic struct
func (ds *DynamicStruct) AddField(name string, typ reflect.Type, tag string) error {
if ds.fieldNames[name] {
return fmt.Errorf("field %s already exists", name)
}
field := reflect.StructField{
Name: name,
Type: typ,
Tag: reflect.StructTag(tag),
}
ds.fields = append(ds.fields, field)
ds.fieldNames[name] = true
return nil
}
// Build creates a new struct type from the defined fields
func (ds *DynamicStruct) Build() reflect.Type {
return reflect.StructOf(ds.fields)
}
// New creates a new instance of the struct
func (ds *DynamicStruct) New() reflect.Value {
return reflect.New(ds.Build()).Elem()
}
func main() {
// Create a dynamic struct for database mapping
dynamicStruct := NewDynamicStruct()
// Add fields based on database schema
dynamicStruct.AddField("ID", reflect.TypeOf(int64(0)), `json:"id" db:"id"`)
dynamicStruct.AddField("Name", reflect.TypeOf(""), `json:"name" db:"name"`)
dynamicStruct.AddField("Email", reflect.TypeOf(""), `json:"email" db:"email"`)
dynamicStruct.AddField("CreatedAt", reflect.TypeOf(int64(0)), `json:"created_at" db:"created_at"`)
// Create a new instance
instance := dynamicStruct.New()
// Set field values
instance.FieldByName("ID").SetInt(1001)
instance.FieldByName("Name").SetString("Dynamic User")
instance.FieldByName("Email").SetString("[email protected]")
instance.FieldByName("CreatedAt").SetInt(1628097357)
// Print the struct type and values
fmt.Printf("Struct Type: %v\n", instance.Type())
fmt.Printf("Field Names: %v\n", instance.Type().NumField())
for i := 0; i < instance.NumField(); i++ {
field := instance.Type().Field(i)
value := instance.Field(i)
fmt.Printf("%s (%s): %v [tags: %s]\n", field.Name, field.Type, value.Interface(), field.Tag)
}
// Convert to a map (e.g., for JSON serialization)
valueMap := make(map[string]interface{})
for i := 0; i < instance.NumField(); i++ {
field := instance.Type().Field(i)
value := instance.Field(i)
valueMap[field.Name] = value.Interface()
}
fmt.Printf("\nAs Map: %v\n", valueMap)
}
Output:
Struct Type: struct { ID int64; Name string; Email string; CreatedAt int64 }
Field Names: 4
ID (int64): 1001 [tags: json:"id" db:"id"]
Name (string): Dynamic User [tags: json:"name" db:"name"]
Email (string): [email protected] [tags: json:"email" db:"email"]
CreatedAt (int64): 1628097357 [tags: json:"created_at" db:"created_at"]
As Map: map[CreatedAt:1628097357 Email:[email protected] ID:1001 Name:Dynamic User]
This technique is particularly useful for ORM libraries, data mappers, and scenarios where the exact structure of data isn’t known until runtime.
Custom JSON Marshaling with Reflection
Reflection enables building custom serialization systems that can handle complex requirements:
package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// CustomMarshaler provides custom JSON marshaling with advanced options
type CustomMarshaler struct {
// FieldNameMapping defines how struct field names are transformed
// e.g., "FieldName" -> "field_name"
FieldNameMapping func(string) string
// OmitEmpty controls whether empty fields are omitted
OmitEmpty bool
// IncludeFields specifies which fields to include (if empty, include all)
IncludeFields map[string]bool
// ExcludeFields specifies which fields to exclude
ExcludeFields map[string]bool
}
// NewCustomMarshaler creates a new custom marshaler
func NewCustomMarshaler() *CustomMarshaler {
return &CustomMarshaler{
FieldNameMapping: func(s string) string { return s },
OmitEmpty: false,
IncludeFields: make(map[string]bool),
ExcludeFields: make(map[string]bool),
}
}
// WithSnakeCase configures the marshaler to use snake_case field names
func (m *CustomMarshaler) WithSnakeCase() *CustomMarshaler {
m.FieldNameMapping = func(s string) string {
var result strings.Builder
for i, r := range s {
if i > 0 && 'A' <= r && r <= 'Z' {
result.WriteByte('_')
}
result.WriteRune(r)
}
return strings.ToLower(result.String())
}
return m
}
// WithOmitEmpty configures the marshaler to omit empty fields
func (m *CustomMarshaler) WithOmitEmpty() *CustomMarshaler {
m.OmitEmpty = true
return m
}
// WithIncludeFields specifies which fields to include
func (m *CustomMarshaler) WithIncludeFields(fields ...string) *CustomMarshaler {
for _, field := range fields {
m.IncludeFields[field] = true
}
return m
}
// WithExcludeFields specifies which fields to exclude
func (m *CustomMarshaler) WithExcludeFields(fields ...string) *CustomMarshaler {
for _, field := range fields {
m.ExcludeFields[field] = true
}
return m
}
// Marshal converts a struct to a map using reflection
func (m *CustomMarshaler) Marshal(v interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil, fmt.Errorf("cannot marshal nil pointer")
}
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot marshal non-struct type: %s", val.Type())
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldVal := val.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Check include/exclude lists
if len(m.IncludeFields) > 0 && !m.IncludeFields[field.Name] {
continue
}
if m.ExcludeFields[field.Name] {
continue
}
// Check if field is empty and should be omitted
if m.OmitEmpty && isEmptyValue(fieldVal) {
continue
}
// Get field name from tag or transform the struct field name
jsonTag := field.Tag.Get("json")
var fieldName string
if jsonTag != "" && jsonTag != "-" {
parts := strings.SplitN(jsonTag, ",", 2)
fieldName = parts[0]
} else {
fieldName = m.FieldNameMapping(field.Name)
}
// Get the field value
var fieldValue interface{}
// Handle nested structs recursively
if fieldVal.Kind() == reflect.Struct && field.Anonymous {
// For embedded structs, merge fields into parent
nestedMap, err := m.Marshal(fieldVal.Interface())
if err != nil {
return nil, err
}
for k, v := range nestedMap {
result[k] = v
}
continue
} else if fieldVal.Kind() == reflect.Struct {
// For regular struct fields, create a nested map
nestedMap, err := m.Marshal(fieldVal.Interface())
if err != nil {
return nil, err
}
fieldValue = nestedMap
} else {
// For other types, use the value directly
fieldValue = fieldVal.Interface()
}
result[fieldName] = fieldValue
}
return result, nil
}
// MarshalJSON implements the json.Marshaler interface
func (m *CustomMarshaler) MarshalJSON(v interface{}) ([]byte, error) {
mapped, err := m.Marshal(v)
if err != nil {
return nil, err
}
return json.Marshal(mapped)
}
// isEmptyValue checks if a value is empty (zero value or empty collection)
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
// Example types for demonstration
type Address struct {
Street string `json:"street"`
City string `json:"city"`
PostalCode string `json:"postal_code"`
}
type Contact struct {
Email string `json:"email"`
Phone string `json:"phone"`
}
type Person struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
Address Address `json:"address"`
Contact Contact `json:"contact"`
Notes string `json:"notes,omitempty"`
Internal string `json:"-"` // Excluded by JSON tag
private string // Unexported field
}
func main() {
person := Person{
ID: 1001,
FirstName: "John",
LastName: "Doe",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Anytown",
PostalCode: "12345",
},
Contact: Contact{
Email: "[email protected]",
Phone: "",
},
Notes: "",
Internal: "Internal data",
private: "Private data",
}
// Standard JSON marshaling
standardJSON, _ := json.MarshalIndent(person, "", " ")
fmt.Println("Standard JSON:")
fmt.Println(string(standardJSON))
// Custom marshaling with snake_case and omit empty
customMarshaler := NewCustomMarshaler().
WithSnakeCase().
WithOmitEmpty().
WithExcludeFields("Age")
customJSON, _ := customMarshaler.MarshalJSON(person)
fmt.Println("\nCustom JSON (snake_case, omit empty, exclude Age):")
fmt.Println(string(customJSON))
// Custom marshaling with only specific fields
limitedMarshaler := NewCustomMarshaler().
WithIncludeFields("FirstName", "LastName", "Contact")
limitedJSON, _ := limitedMarshaler.MarshalJSON(person)
fmt.Println("\nLimited JSON (only FirstName, LastName, Contact):")
fmt.Println(string(limitedJSON))
}
Output:
Standard JSON:
{
"id": 1001,
"first_name": "John",
"last_name": "Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"postal_code": "12345"
},
"contact": {
"email": "[email protected]",
"phone": ""
}
}
Custom JSON (snake_case, omit empty, exclude Age):
{"address":{"city":"anytown","postal_code":"12345","street":"123 main st"},"contact":{"email":"[email protected]"},"first_name":"john","id":1001,"last_name":"doe"}
Limited JSON (only FirstName, LastName, Contact):
{"Contact":{"email":"[email protected]","phone":""},"FirstName":"John","LastName":"Doe"}
This custom marshaling system demonstrates how reflection can be used to build flexible serialization systems that go beyond what’s available in the standard library.
These advanced reflection techniques showcase the power of Go’s reflection system for building flexible, dynamic systems. However, it’s important to remember that reflection comes with performance costs and reduced type safety. In the next section, we’ll explore code generation as an alternative approach that moves complexity from runtime to build time.