Reflection is a powerful tool in Go, but using it incorrectly can lead to performance issues and code that’s difficult to understand.

Let’s see reflection in action. Imagine you have a generic function that needs to set a field on a struct based on a string name.

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Name string
	Age  int
}

func setField(obj interface{}, fieldName string, value interface{}) error {
	// Get the reflect.Value of the object
	v := reflect.ValueOf(obj)

	// If it's a pointer, dereference it to get the underlying struct
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	// Get the reflect.Value of the field by name
	field := v.FieldByName(fieldName)

	// Check if the field exists and is settable
	if !field.IsValid() {
		return fmt.Errorf("field %s not found", fieldName)
	}
	if !field.CanSet() {
		return fmt.Errorf("field %s is not settable", fieldName)
	}

	// Convert the value to the field's type
	val := reflect.ValueOf(value)
	if val.Type().ConvertibleTo(field.Type()) {
		field.Set(val.Convert(field.Type()))
	} else {
		return fmt.Errorf("cannot convert %v to %s", value, field.Type())
	}

	return nil
}

func main() {
	user := User{Name: "Alice", Age: 30}

	fmt.Printf("Before: %+v\n", user)

	err := setField(&user, "Name", "Bob")
	if err != nil {
		fmt.Println("Error:", err)
	}
	err = setField(&user, "Age", 31)
	if err != nil {
		fmt.Println("Error:", err)
	}

	fmt.Printf("After: %+v\n", user)
}

Running this will output:

Before: {Name:Alice Age:30}
After: {Name:Bob Age:31}

Here, setField takes an interface{}, which is Go’s way of saying "any type." Inside, reflect.ValueOf(obj) gives us a reflect.Value that lets us inspect and manipulate the underlying data. We check if it’s a pointer (reflect.Ptr) and dereference it with v.Elem() to work with the actual struct. Then, v.FieldByName(fieldName) lets us get a handle to the specific field we want to modify. Crucially, field.IsValid() and field.CanSet() are essential checks to ensure we’re not trying to access non-existent fields or modify unexported ones. Finally, field.Set() performs the actual modification, after ensuring the input value can be converted to the field’s type.

This mechanism allows you to write highly dynamic code. You can build generic data binding libraries, ORM mappers, or serialization tools that can operate on any struct without knowing its exact type at compile time. The core problem reflection solves is bridging the gap between compile-time type safety and runtime flexibility. When you don’t know the shape of your data until it arrives, reflection is your primary tool.

The real power comes from understanding the reflect package’s types: reflect.Value and reflect.Type. A reflect.Value represents a value of any type, and it has methods like Kind(), Interface(), Set(), FieldByName(), Len(), MapIndex(), etc. A reflect.Type represents a type, and it has methods like Name(), Kind(), NumField(), Field(i), etc. You often obtain a reflect.Type from a reflect.Value using v.Type().

When you encounter a situation where you need to inspect or modify data based on its runtime characteristics rather than its compile-time definition, reflection is the path. This is common in frameworks, middleware, or any place where you need to abstract away the concrete types. Think of serialization (like JSON marshaling/unmarshaling), data validation where rules are defined externally, or even dependency injection systems.

The performance cost of reflection is significant because it bypasses the Go compiler’s optimizations. Every call to a reflection method involves runtime type checks and indirections that would normally be handled by the compiler. For example, v.FieldByName("FieldName") has to search for the field by name at runtime, which is much slower than accessing myStruct.FieldName directly, where the compiler knows the exact memory offset. field.Set(val) also involves runtime checks to ensure type compatibility and mutability.

Avoid reflection when you can achieve the same result with static typing. If you know the types you’re working with at compile time, use standard Go constructs. For instance, if you’re processing a specific User struct, directly access its fields: user.Name = "Bob". If you need to perform an operation on a set of known types, consider using interfaces and type assertions or type switches. For example, if you have a Shape interface with Area() and Perimeter() methods, you can easily implement these for Circle and Rectangle and process them polymorphically without reflection.

The most subtle performance trap is repeated calls to reflect.ValueOf or reflect.TypeOf within a loop. Even if the underlying object’s type doesn’t change, calling these functions repeatedly incurs overhead. It’s always better to obtain the reflect.Value and reflect.Type once before entering the loop and reuse them. This is especially true for methods that are called frequently, like those in serialization or data processing pipelines.

When you need to dynamically unmarshal data into a struct where the structure isn’t known until runtime (e.g., from a dynamic configuration file or an external API with a variable schema), reflection is often the most practical solution. The encoding/json package, for example, uses reflection extensively to map JSON keys to struct fields.

The next step in understanding dynamic Go code is exploring unsafe pointers, which allow even more direct memory manipulation but come with extreme caution.

Want structured learning?

Take the full Golang course →