La reflexión (o reflection) en Go es la capacidad de un programa para inspeccionar, manipular y definir tipos, valores y estructuras en tiempo de ejecución (runtime). A través del paquete reflect, puedes preguntar: “¿Qué tipo es esta variable?”, “¿Qué campos tiene este struct?” o “Cambia el valor de este campo, aunque no conozca su tipo de antemano”.
En Go, el sistema de tipos es estático; el compilador necesita conocer los tipos para garantizar la seguridad de la memoria. Sin embargo, existen escenarios donde esto es imposible, como cuando escribes un serializador de JSON: no puedes saber qué estructura recibirá el usuario hasta que el programa ya está corriendo. Para resolver esto, reflect permite “romper” la rigidez del tipado estático permitiéndote interactuar con la descripción del tipo (reflect.Type) y el valor real (reflect.Value) de una variable.
Esta es una herramienta que Go implementa “a regañadientes”. Aunque es indispensable para librerías de infraestructura (ORMs, validadores, frameworks de DI), tiene un costo alto. Primero, el rendimiento se desploma: las operaciones con reflect suelen ser de 5 a 10 veces más lentas que el código directo debido a las inderecciones y las asignaciones en el heap que provocan. Segundo, pierdes la seguridad del compilador: un error de lógica con tipos en reflect no será un error de compilación, sino un panic catastrófico en producción. Por último, con la llegada de Generics [disponible desde Go 1.18], muchos casos de uso que antes requerían reflexión (como manipular colecciones de tipos heterogéneos de forma segura) ahora se resuelven de forma más eficiente y segura con tipos paramétricos.
package main
import (
"errors"
"fmt"
"reflect"
)
// Validator define una etiqueta personalizada para nuestro ejemplo.
const tagRequired = "required"
// User es un struct de ejemplo con tags de validación.
type User struct {
ID int `validate:"required"`
Name string `validate:"required"`
Email string `validate:"optional"`
}
// ValidateStruct inspecciona un struct y devuelve un error si un campo
// marcado con `required` tiene un valor "zero-valued".
func ValidateStruct(s any) error {
// Obtenemos la representación reflectiva del valor.
v := reflect.ValueOf(s)
// Si el valor es un puntero, obtenemos el elemento al que apunta.
// Esto es crucial para permitir tanto `User{}` como `&User{}`.
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// Si lo que recibimos no es un struct, no podemos iterar sus campos.
if v.Kind() != reflect.Struct {
return errors.New("el argumento debe ser un struct o un puntero a un struct")
}
t := v.Type() // Obtenemos la información del tipo (metadatos).
for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
fieldType := t.Field(i)
// Buscamos la etiqueta 'validate' en el tag del campo.
tag := fieldType.Tag.Get("validate")
if tag == tagRequired {
// IsZero verifica si el campo tiene su valor por defecto
// (0 para int, "" para string, nil para pointers, etc.)
if fieldValue.IsZero() {
return fmt.Errorf("el campo '%s' es obligatorio y está vacío", fieldType.Name)
}
}
}
return nil
}
func main() {
// Caso 1: Struct válido
userOk := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
if err := ValidateStruct(userOk); err != nil {
fmt.Printf("Error en userOk: %v\n", err)
} else {
fmt.Println("userOk es válido")
}
// Caso 2: Struct con campo obligatorio vacío (int 0)
userBad := User{ID: 0, Name: "Bob"}
if err := ValidateStruct(userBad); err != nil {
fmt.Printf("Error en userBad: %v\n", err)
}
// Caso 3: Struct con campo obligatorio vacío (string "")
userBad2 := User{ID: 10, Name: ""}
if err := ValidateStruct(userBad2); err != nil {
fmt.Printf("Error en userBad2: %v\n", err)
}
}
Desglose del ejemplo
En la función ValidateStruct, el primer paso crítico es determinar si lo que recibimos es un valor o un puntero. Fíjate en v.Kind() == reflect.Ptr. En Go, un puntero y el valor al que apunta tienen representaciones distintas en la memoria; si intentas iterar los campos de un puntero directamente, fallará. Usamos v.Elem() para “entrar” en la estructura de datos.
Luego, diferenciamos entre v.Type() y v.Field(i). v.Type() nos da los metadatos (los nombres de los campos y sus tags como validate:"required"), mientras que v.Field(i) nos da el valor real contenido en esa posición de memoria. Esta distinción es fundamental: la reflexión separa la estructura del contenido.
Finalmente, utilizamos fieldValue.IsZero(). Este método es una abstracción muy potente de la runtime que sabe internamente qué significa “vacío” para cada tipo: para un int es 0, para un string es "" y para un struct es una instancia sin inicializar.
El error frecuente
Un error clásico al trabajar con reflect ocurre cuando intentas modificar un valor sin haber pasado un puntero, o cuando intentas llamar a métodos que requieren que el valor sea “adresable”.
// ESTO CAUSARÁ UN PANIC
func SetAge(u User) {
v := reflect.ValueOf(u) // v es una copia del struct, no el original
if v.Kind() == reflect.Struct {
// v.Field(0) es una copia, no es "settable" (ajustable)
v.Field(0).SetInt(25) // panic: reflect.Value.SetInt using un valor no addressable
}
}
Si quieres modificar un campo mediante reflexión, reflect.ValueOf(u) debe recibir un puntero (&u). Además, incluso si recibes un puntero, debes llamar a .Elem() para obtener el valor al que apunta antes de intentar usar .SetInt() o cualquier método de escritura. Si el valor no es addressable (es decir, no tiene una dirección de memoria de la cual puedas tomar el control para escribir), la ejecución abortará con un panic.
N° 175