Skip to main content

Go Reflection

Introduction

Reflection is a powerful feature in Go that allows programs to inspect and manipulate their own structure at runtime. It's like holding up a mirror to your code, enabling you to examine types, values, and functions that weren't necessarily known when you wrote the program.

While Go is primarily a statically typed language, reflection provides a way to work with types dynamically. This capability is particularly useful when dealing with:

  • Generic programming scenarios
  • Processing data in formats like JSON or XML
  • Building frameworks and libraries
  • Creating testing tools

However, reflection comes with some trade-offs. It makes code more complex, bypasses type safety, and can impact performance. As the Go proverb says:

Clear is better than clever. Reflection is never clear.

In this guide, we'll explore Go's reflection system, understand its practical applications, and learn when (and when not) to use it.

The reflect Package

Go provides reflection capabilities through the standard library's reflect package. This package defines two important types:

  1. Type: Represents the type of a Go value
  2. Value: Represents the actual value

Let's start with a simple example to see reflection in action:

go
package main

import (
"fmt"
"reflect"
)

func main() {
// Create a string variable
message := "Hello, Reflection!"

// Get the type and value using reflection
messageType := reflect.TypeOf(message)
messageValue := reflect.ValueOf(message)

// Print the type and value
fmt.Println("Type:", messageType)
fmt.Println("Value:", messageValue)
fmt.Println("Kind:", messageValue.Kind())
}

Output:

Type: string
Value: Hello, Reflection!
Kind: string

This simple example demonstrates how to:

  1. Import the reflect package
  2. Use reflect.TypeOf() to get the type of a variable
  3. Use reflect.ValueOf() to get a reflection-enabled value
  4. Access information about the type and value

The Three Laws of Reflection

Go's reflection follows three important laws that help understand how it works:

1. Reflection goes from interface to reflection object

When you call reflect.TypeOf() or reflect.ValueOf(), you're passing a value that's converted to an empty interface (interface{}). The reflection functions then extract type information from this interface value.

2. Reflection goes from reflection object to interface

You can convert a reflect.Value back to an interface using the Interface() method.

go
package main

import (
"fmt"
"reflect"
)

func main() {
// Start with a string
original := "Hello, World!"

// Convert to reflect.Value
reflectValue := reflect.ValueOf(original)

// Convert back to interface{}
interfaceValue := reflectValue.Interface()

// Type assert back to string
returnedString := interfaceValue.(string)

fmt.Println(returnedString) // "Hello, World!"
}

Output:

Hello, World!

3. To modify a reflection object, it must be settable

If you want to modify a value through reflection, it must be "settable". A reflect.Value is settable only if it refers to an actual memory location that can be modified.

Let's see an example of what works and what doesn't:

go
package main

import (
"fmt"
"reflect"
)

func main() {
// Example 1: Trying to modify a non-settable value (won't work)
x := 3.14
v := reflect.ValueOf(x)

// This will panic
fmt.Println("Can set x?", v.CanSet())
// Uncommenting the next line would cause a panic
// v.SetFloat(2.71)

// Example 2: Using a pointer to make a value settable
y := 3.14
p := reflect.ValueOf(&y) // Get the reflection value of the pointer
v = p.Elem() // Dereference it to get the pointed-to value

fmt.Println("Can set y?", v.CanSet())
v.SetFloat(2.71) // This works!

fmt.Println("y after modification:", y)
}

Output:

Can set x? false
Can set y? true
y after modification: 2.71

Notice that:

  1. When we try to modify x directly, it's not settable
  2. When we use a pointer to y and dereference it with Elem(), we can modify the value

Inspecting Structs with Reflection

Reflection is particularly useful for inspecting struct types. Let's see how to explore the fields of a struct:

go
package main

import (
"fmt"
"reflect"
)

type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"min=0,max=130"`
Address string `json:"address,omitempty"`
}

func main() {
p := Person{
Name: "Alice",
Age: 30,
Address: "Wonderland",
}

// Get the reflection value and type
v := reflect.ValueOf(p)
t := v.Type()

// Iterate over the struct fields
fmt.Println("Fields of Person struct:")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)

fmt.Printf(" %s: %v (type: %v)
", field.Name, value, field.Type)
fmt.Printf("
", field.Tag)
}
}

Output:

Fields of Person struct:
Name: Alice (type: string)

Age: 30 (type: int)

Address: Wonderland (type: string)

This example shows how to:

  1. Iterate through struct fields using reflection
  2. Access field names, types, and values
  3. Retrieve struct tags

Modifying Structs with Reflection

Let's see how to modify a struct's fields using reflection:

go
package main

import (
"fmt"
"reflect"
)

type Product struct {
Name string
Price float64
Stock int
}

func main() {
// Create a Product instance
product := &Product{
Name: "Laptop",
Price: 999.99,
Stock: 5,
}

// Print the initial product
fmt.Printf("Initial product: %+v
", product)

// Get a reflect.Value for the pointer and dereference it
v := reflect.ValueOf(product).Elem()

// Modify fields using reflection
nameField := v.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
nameField.SetString("High-End Laptop")
}

priceField := v.FieldByName("Price")
if priceField.IsValid() && priceField.CanSet() {
priceField.SetFloat(1299.99)
}

// Print the modified product
fmt.Printf("Modified product: %+v
", product)
}

Output:

Initial product: &{Name:Laptop Price:999.99 Stock:5}
Modified product: &{Name:High-End Laptop Price:1299.99 Stock:5}

Notice that:

  1. We use a pointer to the struct to make it modifiable
  2. We access fields by name using FieldByName()
  3. We check if the field is valid and settable before modifying it

Working with Functions Using Reflection

Reflection can also be used to work with functions dynamically. Let's see how to inspect and call functions:

go
package main

import (
"fmt"
"reflect"
)

func Add(a, b int) int {
return a + b
}

func Greet(name string) string {
return "Hello, " + name + "!"
}

func InspectFunction(fn interface{}) {
// Get the function type
fnType := reflect.TypeOf(fn)

// Check if it's actually a function
if fnType.Kind() != reflect.Func {
fmt.Println("Not a function!")
return
}

// Print the number of input parameters and return values
fmt.Printf("Function has %d input parameters and %d return values
",
fnType.NumIn(), fnType.NumOut())

// Print each input parameter type
for i := 0; i < fnType.NumIn(); i++ {
fmt.Printf(" Input #%d: %v
", i, fnType.In(i))
}

// Print each return value type
for i := 0; i < fnType.NumOut(); i++ {
fmt.Printf(" Output #%d: %v
", i, fnType.Out(i))
}
}

func CallFunction(fn interface{}, args ...interface{}) []reflect.Value {
// Convert the function to a reflect.Value
fnValue := reflect.ValueOf(fn)

// Prepare the arguments
reflectArgs := make([]reflect.Value, len(args))
for i, arg := range args {
reflectArgs[i] = reflect.ValueOf(arg)
}

// Call the function
return fnValue.Call(reflectArgs)
}

func main() {
// Inspect functions
fmt.Println("Inspecting Add function:")
InspectFunction(Add)

fmt.Println("
Inspecting Greet function:")
InspectFunction(Greet)

// Call functions using reflection
fmt.Println("
Calling functions using reflection:")
addResult := CallFunction(Add, 5, 7)
fmt.Printf("Add(5, 7) = %v
", addResult[0].Interface())

greetResult := CallFunction(Greet, "Reflection")
fmt.Printf("Greet(\"Reflection\") = %v
", greetResult[0].Interface())
}

Output:

Inspecting Add function:
Function has 2 input parameters and 1 return values
Input #0: int
Input #1: int
Output #0: int

Inspecting Greet function:
Function has 1 input parameters and 1 return values
Input #0: string
Output #0: string

Calling functions using reflection:
Add(5, 7) = 12
Greet("Reflection") = Hello, Reflection!

This example demonstrates:

  1. How to inspect a function's signature using reflection
  2. How to dynamically call functions with arguments

Practical Example: A Simple ORM

Let's see a practical example of how reflection might be used to build a simple object-relational mapping (ORM) system. This example will show how to generate SQL statements from Go structs:

go
package main

import (
"fmt"
"reflect"
"strings"
)

// Tag name for database fields
const dbTag = "db"

// Generate an INSERT SQL statement for any struct
func GenerateInsertSQL(obj interface{}) (string, []interface{}) {
// Get the reflect.Value and reflect.Type
val := reflect.ValueOf(obj)
typ := val.Type()

// If it's a pointer, dereference it
if typ.Kind() == reflect.Ptr {
val = val.Elem()
typ = val.Type()
}

// Check if it's a struct
if typ.Kind() != reflect.Struct {
panic("GenerateInsertSQL expects a struct or a pointer to a struct")
}

// Get the struct name as the table name
tableName := strings.ToLower(typ.Name())

// Collect field names and values
var fields []string
var placeholders []string
var values []interface{}

for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)

// Get the database field name from the tag, or use the field name
dbFieldName := field.Tag.Get(dbTag)
if dbFieldName == "" || dbFieldName == "-" {
// Skip fields with no db tag or with db:"-"
continue
}

// Add the field and value
fields = append(fields, dbFieldName)
placeholders = append(placeholders, "?")
values = append(values, val.Field(i).Interface())
}

// Build the SQL statement
sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
tableName,
strings.Join(fields, ", "),
strings.Join(placeholders, ", "),
)

return sql, values
}

// Our example database model
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Email string `db:"email"`
CreatedAt string `db:"created_at"`
IsActive bool `db:"is_active"`
Score float64
// Note: Score has no db tag, so it will be skipped
}

func main() {
// Create a user instance
user := User{
ID: 1,
Username: "gopher",
Email: "[email protected]",
CreatedAt: "2023-08-15T10:30:00Z",
IsActive: true,
Score: 42.5, // This will be ignored in SQL generation
}

// Generate SQL
sql, values := GenerateInsertSQL(user)

// Print the results
fmt.Println("Generated SQL:")
fmt.Println(sql)
fmt.Println("
Values:")
for i, value := range values {
fmt.Printf(" %d: %v (type: %T)
", i, value, value)
}
}

Output:

Generated SQL:
INSERT INTO user (id, username, email, created_at, is_active) VALUES (?, ?, ?, ?, ?)

Values:
0: 1 (type: int)
1: gopher (type: string)
2: [email protected] (type: string)
3: 2023-08-15T10:30:00Z (type: string)
4: true (type: bool)

This practical example shows how reflection enables:

  1. Dynamic SQL generation based on struct fields
  2. Integration of field tags for mapping between Go and database fields
  3. Type-safe extraction of field values for parameterized queries

When to Use Reflection (and When Not to)

Reflection is a powerful tool, but it should be used judiciously. Here are some guidelines:

Good Use Cases for Reflection:

  1. Serialization/Deserialization: When implementing encoding/decoding for formats like JSON, XML, or custom protocols.

  2. Framework Development: When building generic frameworks where the types aren't known in advance.

  3. Testing Frameworks: For inspecting code structure and behavior during testing.

  4. Dependency Injection: When implementing container systems that need to instantiate and configure objects based on configuration.

When to Avoid Reflection:

  1. Performance-Critical Code: Reflection operations are significantly slower than direct operations.

  2. Simple Problems: If there's a straightforward solution without reflection, prefer that.

  3. Type-Safe Alternatives Exist: Go's interfaces often provide type-safe alternatives to reflection.

  4. When Code Clarity is Crucial: Reflection makes code harder to understand and debug.

go
// Example: A simple solution without reflection
func PrintDetails(items []interface{}) {
for _, item := range items {
fmt.Printf("Value: %v
", item)
}
}

// Versus type switches (often clearer for simple cases)
func PrintDetailsTypeSwitch(items []interface{}) {
for _, item := range items {
switch v := item.(type) {
case int:
fmt.Printf("Integer: %d
", v)
case string:
fmt.Printf("String: %s
", v)
case bool:
fmt.Printf("Boolean: %t
", v)
default:
fmt.Printf("Unknown type: %v
", v)
}
}
}

Reflection Performance Considerations

When using reflection, be aware of the performance implications:

go
package main

import (
"fmt"
"reflect"
"time"
)

func directAccess(iterations int) {
start := time.Now()

type Person struct {
Name string
Age int
}

p := Person{Name: "John", Age: 30}

for i := 0; i < iterations; i++ {
_ = p.Name
_ = p.Age
}

elapsed := time.Since(start)
fmt.Printf("Direct access: %v
", elapsed)
}

func reflectionAccess(iterations int) {
start := time.Now()

type Person struct {
Name string
Age int
}

p := Person{Name: "John", Age: 30}
v := reflect.ValueOf(p)

for i := 0; i < iterations; i++ {
_ = v.FieldByName("Name").String()
_ = v.FieldByName("Age").Int()
}

elapsed := time.Since(start)
fmt.Printf("Reflection access: %v
", elapsed)
}

func main() {
iterations := 1000000

directAccess(iterations)
reflectionAccess(iterations)
}

Output (will vary by system):

Direct access: 426.05µs
Reflection access: 247.326914ms

As you can see, reflection operations can be hundreds of times slower than direct access.

Practical Examples

Example 1: Building a Simple JSON Encoder

go
package main

import (
"fmt"
"reflect"
"strings"
)

func ToJSON(v interface{}) string {
value := reflect.ValueOf(v)

switch value.Kind() {
case reflect.String:
return fmt.Sprintf("\"%s\"", value.String())

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("%d", value.Int())

case reflect.Float32, reflect.Float64:
return fmt.Sprintf("%g", value.Float())

case reflect.Bool:
return fmt.Sprintf("%t", value.Bool())

case reflect.Slice, reflect.Array:
var items []string
for i := 0; i < value.Len(); i++ {
items = append(items, ToJSON(value.Index(i).Interface()))
}
return "[" + strings.Join(items, ", ") + "]"

case reflect.Map:
var parts []string
iter := value.MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
parts = append(parts,
fmt.Sprintf("\"%v\": %s", k, ToJSON(v.Interface())))
}
return "{" + strings.Join(parts, ", ") + "}"

case reflect.Struct:
var fields []string
typ := value.Type()
for i := 0; i < value.NumField(); i++ {
field := typ.Field(i)
fieldValue := value.Field(i)

// Get JSON field name from tag or use field name
jsonName := field.Tag.Get("json")
if jsonName == "" {
jsonName = field.Name
}

fields = append(fields,
fmt.Sprintf("\"%s\": %s", jsonName, ToJSON(fieldValue.Interface())))
}
return "{" + strings.Join(fields, ", ") + "}"

case reflect.Ptr:
if value.IsNil() {
return "null"
}
return ToJSON(value.Elem().Interface())

default:
return fmt.Sprintf("\"%v\"", value)
}
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
}

func main() {
// Try with different data types
fmt.Println(ToJSON("hello"))
fmt.Println(ToJSON(42))
fmt.Println(ToJSON(true))
fmt.Println(ToJSON([]int{1, 2, 3}))
fmt.Println(ToJSON(map[string]int{"a": 1, "b": 2}))

// Try with a struct
person := Person{
Name: "Alice",
Age: 30,
Address: "Wonderland",
}
fmt.Println(ToJSON(person))
}

Output:

"hello"
42
true
[1, 2, 3]
{"a": 1, "b": 2}
{"name": "Alice", "age": 30, "address": "Wonderland"}

Example 2: Dynamic Function Decorator

go
package main

import (
"fmt"
"reflect"
"time"
)

// A function decorator that adds timing to any function
func TimingDecorator(fn interface{}) interface{} {
// Get function value and type
fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()

// Check if it's a function
if fnType.Kind() != reflect.Func {
panic("TimingDecorator expects a function")
}

// Create a new function with the same signature
decoratedFuncValue := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
// Start timing
start := time.Now()

// Call the original function
results := fnValue.Call(args)

// End timing
elapsed := time.Since(start)

// Print timing information
fmt.Printf("Function execution took %v
", elapsed)

// Return the original results
return results
})

// Return the new function
return decoratedFuncValue.Interface()
}

// Some example functions to decorate
func Add(a, b int) int {
return a + b
}

func SlowFunction(n int) int {
sum := 0
for i := 0; i < n; i++ {
sum += i
}
return sum
}

func main() {
// Decorate the functions
decoratedAdd := TimingDecorator(Add).(func(int, int) int)
decoratedSlow := TimingDecorator(SlowFunction).(func(int) int)

// Call the decorated functions
result1 := decoratedAdd(5, 7)
fmt.Printf("Result: %d

", result1)

result2 := decoratedSlow(1000000)
fmt.Printf("Result: %d
", result2)
}

Output:

Function execution took 592ns
Result: 12

Function execution took 14.2095ms
Result: 499999500000

Summary

Go's reflection capabilities provide powerful tools for inspecting and manipulating programs at runtime. Through the reflect package, you can examine types, access and modify values, work with structs, and even call functions dynamically.

Key takeaways from this guide:

  1. Reflection allows your Go programs to examine and modify their own structure at runtime
  2. The reflect package provides the core functionality through Type and Value types
  3. The three laws of reflection help understand how values flow between Go's type system and the reflection system
  4. Reflection is particularly useful for working with structs, including accessing fields and struct tags
  5. Reflection enables dynamic function calls and inspection
  6. Practical applications include ORMs, serialization frameworks, and testing tools
  7. Reflection should be used judiciously, as it has implications for code clarity and performance

Further Resources

To deepen your understanding of Go reflection, check out these resources:

  1. The Laws of Reflection - The official Go blog post on reflection
  2. reflect Package Documentation - Official documentation for the reflect package
  3. Effective Go - Contains guidance on idiomatic Go code, including when to use reflection

Exercises

  1. Basic Reflection: Write a function that takes any value and prints its type, kind, and whether it can be set using reflection.

  2. Struct Explorer: Create a function that takes a struct and recursively prints all its fields, including nested structs.

  3. Dynamic Configuration: Build a function that populates a struct with values from a map, using reflection to match field names.

  4. Simple Validator: Implement a validation function that checks struct fields based on struct tags (like validate:"required" or validate:"min=5").

  5. Generic Deep Copy: Create a function that makes a deep copy of any value (including structs, maps, and slices) using reflection.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)