La reflexión en Go es la capacidad de un programa para inspeccionar y manipular su propia estructura y valores en tiempo de ejecución. El paquete reflect permite que un programa descubra los tipos de sus variables, sus campos y sus valores, incluso si estos no se conocen en el momento de la compilación. Este mecanismo es lo que permite que librerías como encoding/json puedan mapear un mapa dinámico a una estructura de un usuario sin conocerla de antemano.
El funcionamiento de la reflexión se rige por las tres leyes de Rob Pike, que dictan cómo fluye la información entre el mundo de los tipos estáticos de Go y el mundo dinámico de reflect. El paquete reflect funciona operando sobre descriptores de objetos que residen en el heap, permitiendo que el runtime examine la metadata de los tipos. Debes usarla exclusivamente cuando la lógica de tu programa depende de la estructura de los datos y no de su comportamiento (interfaces), ya que su uso excesivo degrada el rendimiento debido a escapes al heap y pérdida de optimizaciones del compilador. Si intentas manipular un valor sin respetar las reglas de direccionabilidad, el runtime lanzará un panic que no podrás capturar con un error convencional, rompiendo la ejecución.
Las tres leyes se resumen así:
1. De un valor de interfaz a un objeto de reflexión: Al pasar un valor a reflect.ValueOf o reflect.TypeOf, el runtime crea un objeto reflect.Value o reflect.Type que envuelve el valor original.
2. De un objeto de reflexión a un valor de interfaz: Puedes recuperar el valor original mediante el método Interface(), el cual devuelve un any.
3. Modificación de valores: Para modificar un valor mediante reflexión, el objeto de reflexión debe ser “settable” (direccionable). Esto significa que no puedes modificar una copia; necesitas trabajar sobre el puntero original mediante Elem().
package main
import (
"fmt"
"reflect"
)
type Usuario struct {
Nombre string
Edad int
}
func main() {
// Inicializamos nuestro dato de prueba
u := Usuario{Nombre: "Alex", Edad: 30}
// --- LEY 1: De valor (interface) a reflect.Value ---
// reflect.ValueOf(u) crea un descriptor que contiene una COPIA de u.
v := reflect.ValueOf(u)
t := reflect.TypeOf(u)
fmt.Printf("Tipo: %v, Valor: %v\n", t, v)
// --- LEY 2: De reflect.Value a interface (any) ---
// Extraemos el valor de vuelta a su tipo original mediante una aserción.
uCopia := v.Interface().(Usuario)
fmt.Printf("Recuperado mediante Interface(): %+v\n", uCopia)
// --- LEY 3: La ley de la modificabilidad (Settability) ---
// Intentar v.SetInt(31) aquí lanzaría un panic porque v es una copia.
// Para modificar 'u', necesitamos su dirección de memoria.
// 1. Obtenemos el valor del puntero a 'u'
ptrV := reflect.ValueOf(&u) // reflect.Value que contiene *Usuario
// 2. Usamos Elem() para obtener el valor al que apunta el puntero
// Elem() nos da el valor "direccionable" (la instancia real, no la copia del puntero).
elemV := ptrV.Elem()
// Ahora que es direccionable, podemos modificar sus campos.
if elemV.Kind() == reflect.Struct {
campoNombre := elemV.FieldByName("Nombre")
campoEdad := elemV.FieldByName("Edad")
if campoNombre.CanSet() {
campoNombre.SetString("Alexander")
}
if campoEdad.CanSet() {
campoEdad.SetInt(31)
}
}
fmt.Printf("Valor original modificado: %+v\n", u)
}
Desglose del concepto
En el ejemplo anterior, observa cómo interactuamos con la memoria. Cuando ejecutas reflect.ValueOf(u), la variable v no es u, es un objeto que contiene una copia de los datos de u. Por eso, cualquier intento de modificación directa fallaría.
Para cumplir con la Tercera Ley, necesitamos la dirección de memoria de u. Al hacer reflect.ValueOf(&u), obtenemos un reflect.Value que representa un puntero (*Usuario). Sin embargo, el puntero en sí no es lo que queremos cambiar, sino el contenido al que apunta. Aquí es donde ptrV.Elem() es crítico: este método desreferencia el puntero, dándonos acceso al valor real en memoria. El método CanSet() es la forma en que el runtime te advierte si el objeto es direccionable; si CanSet() devuelve false, intentar llamar a SetString o SetInt provocará un panic.
Finalmente, v.Interface() utiliza la información del descriptor para empaquetar el valor en una interfaz de Go (any), permitiéndonos volver al mundo de tipos estáticos mediante una aserción de tipo (.(Usuario)).
El error frecuente
Un error clásico ocurre cuando intentas modificar un campo de una estructura pasando la estructura por valor en lugar de por puntero.
u := Usuario{Nombre: "Error", Edad: 10}
v := reflect.ValueOf(u) // v contiene una COPIA de u
field := v.FieldByName("Edad")
// Esto provocará un panic: "reflect.Value.SetInt: value is not settable"
field.SetInt(20)
El error es sutil: v es un objeto de reflexión válido y field también lo es, pero como el origen fue una copia (valor por paso), el runtime no tiene forma de saber a qué dirección de memoria original pertenece ese campo, por lo tanto, no tiene permiso para escribir en ella.
Cuando la reflexión se vuelve demasiado compleja o difícil de razonar, considera estas tres alternativas:
1. Generación de código: Usa go generate con herramientas como stringer o protobuf. Es más rápido y seguro porque el código se genera antes de la compilación.
2. Generics: Si tu objetivo es escribir algoritmos que funcionen con cualquier tipo (como una función Contains[T comparable](slice []T, val T) bool), los genéricos introducidos en Go 1.18 son la opción preferida, ya que ofrecen seguridad de tipos en tiempo de compilación y rendimiento nativo.
3. Interfaces bien diseñadas: Si necesitas polimorfismo, define una interfaz con los métodos necesarios. La mayoría de las veces, lo que intentas resolver con reflexión se soluciona con un contrato de comportamiento claro.
N° 178