Let’s talk about something in Go that often seems mysterious but is incredibly practical: reflection. I want to share some ways I use it to solve real problems. Think of reflection not as magic, but as a tool that lets a program look at itself. It allows you to work with types and values you don’t know about when you’re writing the code. This is powerful, but it comes with a cost. I use it sparingly, only when the benefit is clear. Here are some specific patterns that have proven useful.
First, why even bother? In Go, we love static types. They make our code safe and fast. But sometimes, you need to write code that works for many different types. Maybe you’re building a library that others will use with their own data structures. That’s where reflection steps in. It’s part of the reflect package, and it lets you inspect and manipulate values based on their runtime type information.
Let’s start with a simple example. Imagine you have a struct, and you want to print out all its fields, their types, and their values. Without knowing the struct ahead of time, you need reflection.
package main
import (
"fmt"
"reflect"
)
func printDetails(item interface{}) {
// Get the reflect.Value, which represents the actual value
val := reflect.ValueOf(item)
// Get the reflect.Type, which holds the type information
typ := val.Type()
fmt.Printf("Inspecting a %v:\n", typ.Name())
// We need to handle pointers. If it's a pointer, get the element it points to.
if val.Kind() == reflect.Ptr {
val = val.Elem()
typ = val.Type()
}
// Check if it's actually a struct. We can only iterate fields on structs.
if val.Kind() != reflect.Struct {
fmt.Println("This is not a struct.")
return
}
// NumField() tells us how many fields the struct has.
for i := 0; i < val.NumField(); i++ {
// Get information about the i-th field.
fieldType := typ.Field(i)
// Get the value of the i-th field.
fieldValue := val.Field(i)
// Print the field's name, its type, and its current value.
fmt.Printf(" Field %d: %s (Type: %v) = %v\n",
i, fieldType.Name, fieldType.Type, fieldValue.Interface())
}
}
type ServerConfig struct {
Host string
Port int
Enabled bool
}
func main() {
config := ServerConfig{Host: "app.example.com", Port: 443, Enabled: true}
printDetails(config)
// Also works with a pointer to the struct.
printDetails(&config)
}
Running this would show you all the details of the ServerConfig. This is the foundation. You’re asking the program, “What are you?” at runtime, and it’s telling you. I use patterns like this for logging, debugging, and writing generic utilities.
A very common use is dynamic serialization and deserialization. Let’s say you’re reading a configuration file, like YAML or JSON, and you need to load it into a struct. You could write a parser for every single config struct, or you can write one function that uses reflection to do it for any struct. The key is using struct tags.
package main
import (
"fmt"
"reflect"
"strconv"
)
// This is a simplified loader. A real one would parse a file.
func loadFromMap(data map[string]string, config interface{}) error {
v := reflect.ValueOf(config)
// We expect a pointer to a struct so we can modify it.
if v.Kind() != reflect.Ptr {
return fmt.Errorf("config must be a pointer to a struct")
}
v = v.Elem() // Dereference the pointer to get the struct value.
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldInfo := t.Field(i)
// Look for a "map" tag on the struct field.
key := fieldInfo.Tag.Get("map")
if key == "" {
// If no tag, use the field name as the key.
key = fieldInfo.Name
}
// Find the value in our data map.
strValue, exists := data[key]
if !exists {
continue // This key wasn't in the config map.
}
// Now we need to set the field's value, converting from string.
// The type of the field (field.Kind()) tells us what to convert to.
switch field.Kind() {
case reflect.String:
field.SetString(strValue)
case reflect.Int:
intVal, err := strconv.Atoi(strValue)
if err != nil {
return fmt.Errorf("field %s: %v", fieldInfo.Name, err)
}
field.SetInt(int64(intVal))
case reflect.Bool:
boolVal, err := strconv.ParseBool(strValue)
if err != nil {
return fmt.Errorf("field %s: %v", fieldInfo.Name, err)
}
field.SetBool(boolVal)
// ... handle more types (float, slices, etc.)
default:
return fmt.Errorf("unsupported type %v for field %s", field.Kind(), fieldInfo.Name)
}
}
return nil
}
type AppConfig struct {
Name string `map:"app_name"`
Workers int `map:"worker_count"`
Debug bool
}
func main() {
// Simulate data read from a file.
configData := map[string]string{
"app_name": "MyGoService",
"worker_count": "4",
"Debug": "true", // Matches field name 'Debug'
}
var cfg AppConfig
err := loadFromMap(configData, &cfg)
if err != nil {
panic(err)
}
fmt.Printf("Loaded config: %+v\n", cfg)
// Output: Loaded config: {Name:MyGoService Workers:4 Debug:true}
}
This pattern is everywhere. Libraries like JSON and YAML encoders use it. You define your struct with tags, and the library’s reflection code does the heavy lifting of matching fields to data keys.
Another lifesaver is setting defaults. When you have a large config struct, setting zero-value defaults in code can be messy. Instead, you can tag fields with their default values and use reflection to apply them.
func applyDefaults(s interface{}) {
v := reflect.ValueOf(s).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldInfo := t.Field(i)
// Only set default if the field is at its zero value.
if !field.IsZero() {
continue
}
defaultTag := fieldInfo.Tag.Get("default")
if defaultTag == "" {
continue
}
// Use a helper to set the value based on the tag string.
// This is similar to the switch statement in the previous example.
setFieldFromString(field, defaultTag)
}
}
type ServiceConfig struct {
TimeoutSecs int `default:"30"`
LogLevel string `default:"INFO"`
CacheSize int // No default tag, zero value (0) will remain.
}
Validation is a natural extension of this. You can check if a field’s value meets rules defined in tags.
type UserRegistration struct {
Username string `validate:"min=3,max=20"`
Email string `validate:"email"`
Age int `validate:"min=18"`
}
func validateStruct(s interface{}) []string {
var errors []string
v := reflect.ValueOf(s).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldInfo := t.Field(i)
rules := fieldInfo.Tag.Get("validate")
// Parse 'rules' string and apply checks to 'field'.
// If a check fails, add to errors:
// errors = append(errors, fmt.Sprintf("%s: failed rule X", fieldInfo.Name))
}
return errors
}
These tag-based patterns keep your structs as the single source of truth. The definition, defaults, and validation rules are all right there together.
Sometimes, you need to call a method, but you only know its name as a string. Reflection can do that. I’ve used this for building simple command routers or plugin systems.
type Calculator struct{}
func (c Calculator) Add(a, b int) int {
return a + b
}
func (c Calculator) Subtract(a, b int) int {
return a - b
}
func executeMethod(obj interface{}, methodName string, args ...interface{}) ([]interface{}, error) {
v := reflect.ValueOf(obj)
m := v.MethodByName(methodName)
// Check if the method was found.
if !m.IsValid() {
return nil, fmt.Errorf("method %s not found", methodName)
}
// Convert arguments to reflect.Value slices for the Call.
in := make([]reflect.Value, len(args))
for i, arg := range args {
in[i] = reflect.ValueOf(arg)
}
// Call the method.
out := m.Call(in)
// Convert the results back to interface{}.
results := make([]interface{}, len(out))
for i, val := range out {
results[i] = val.Interface()
}
return results, nil
}
func main() {
calc := Calculator{}
result, _ := executeMethod(calc, "Add", 5, 3)
fmt.Println(result[0]) // Output: 8
}
This is powerful but should be used cautiously. You lose compile-time safety. If you misspell “Add,” you won’t know until the code runs and fails.
For dependency injection, reflection helps automate wiring. You can scan a struct’s fields, see which ones are interfaces or struct pointers, and create instances to populate them. Many Go DI frameworks work this way under the hood. They look at your struct tags or the field types themselves to figure out what needs to be provided.
When you have an interface{} (the empty interface), you often need to know what’s inside. A type switch is good, but sometimes you need more detail. Reflection gives you that.
func describe(x interface{}) string {
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("an integer: %d", v.Int())
case reflect.Slice:
return fmt.Sprintf("a slice with length %d and element type %v", v.Len(), v.Type().Elem())
case reflect.Struct:
return fmt.Sprintf("a struct named %s", v.Type().Name())
default:
return fmt.Sprintf("a %v", v.Type())
}
}
This is more granular than a standard type switch and lets you handle broad categories of types.
Creating a true, independent copy of a complex structure is tricky. You need to copy every field, and if that field is a slice, map, or pointer, you need to copy what it points to as well. Reflection can walk the entire structure recursively.
func deepCopy(src interface{}) interface{} {
if src == nil {
return nil
}
original := reflect.ValueOf(src)
copy := reflect.New(original.Type()).Elem()
deepCopyValue(original, copy)
return copy.Interface()
}
func deepCopyValue(original, copy reflect.Value) {
switch original.Kind() {
case reflect.Ptr:
// Create a new pointer and copy the pointed-to value.
origVal := original.Elem()
if origVal.IsValid() {
copy.Set(reflect.New(origVal.Type()))
deepCopyValue(origVal, copy.Elem())
}
case reflect.Slice:
// Make a new slice of the same length/capacity.
copy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
// Copy each element.
for i := 0; i < original.Len(); i++ {
deepCopyValue(original.Index(i), copy.Index(i))
}
case reflect.Map:
// Make a new map.
copy.Set(reflect.MakeMap(original.Type()))
// Iterate and copy each key-value pair.
for _, key := range original.MapKeys() {
origVal := original.MapIndex(key)
copyVal := reflect.New(origVal.Type()).Elem()
deepCopyValue(origVal, copyVal)
copy.SetMapIndex(key, copyVal)
}
case reflect.Struct:
// Copy each field of the struct.
for i := 0; i < original.NumField(); i++ {
// Skip unexported fields (CanSet will be false).
if copy.Field(i).CanSet() {
deepCopyValue(original.Field(i), copy.Field(i))
}
}
default:
// For basic types (int, string, bool), a simple assignment works.
copy.Set(original)
}
}
This is a simplified version, but it shows the idea. You check the “kind” of each value and decide how to copy it.
Finally, we must talk about speed. Reflection is slower than direct code. The compiler can’t optimize it. So, a key pattern is to use reflection to set things up once, then use fast, non-reflective code for the hot path.
For example, if you’re using reflection to call methods repeatedly in a loop, don’t look up the MethodByName inside the loop. Do it once and cache the reflect.Method or a function closure.
var methodCache sync.Map // map[reflect.Type]map[string]reflect.Method
func getCachedMethod(obj interface{}, name string) (reflect.Method, bool) {
t := reflect.TypeOf(obj)
typeMethods, _ := methodCache.LoadOrStore(t, make(map[string]reflect.Method))
methods := typeMethods.(map[string]reflect.Method)
if m, ok := methods[name]; ok {
return m, true
}
// Use reflection to find it.
m, ok := t.MethodByName(name)
if ok {
methods[name] = m
}
return m, ok
}
This way, you pay the reflection cost only the first time you need a method for a given type.
Reflection is a sharp tool. It makes some otherwise impossible tasks straightforward. I use it for serialization, configuration, generating boilerplate code, and building flexible frameworks. The patterns I’ve shown—inspecting structs, using tags, calling methods dynamically, copying structures, and caching for performance—are the ones I reach for again and again. They help keep the rest of my code clean and simple. Remember the rule: if you can solve the problem without reflection, do that. But when you need it, these patterns provide a solid, tested way to apply it effectively.