En Go, el tipado es estático y se resuelve principalmente en tiempo de compilación. Sin embargo, cuando construyes librerías de serialización (como encoding/json), ORMs o frameworks de validación, necesitas “romper” esa barrera y preguntar: “¿Qué tipo es esto exactamente y qué contiene?”. Para eso usamos el paquete reflect.
Para entenderlo, primero debemos diferenciar dos conceptos fundamentales: reflect.Type y reflect.Value. El primero es la descripción del tipo (el esquema, el nombre, los tags); el segundo es la representación del valor real en memoria (el dato que puedes leer o incluso modificar si es direccionable).
Cuando pasas cualquier variable a reflect.TypeOf(x), obtienes un objeto que te dice si es un struct, un slice o un int. Pero reflect.TypeOf es solo lectura; no puedes obtener el valor de un campo usando solo el tipo. Ahí entra reflect.ValueOf(x), que es el que realmente te permite navegar por la memoria. El motor de Go utiliza una estructura interna llamada eface (empty interface) que guarda un puntero al tipo y un puntero a los datos. El paquete reflect simplemente expone esa estructura interna de forma segura.
Para usarlo correctamente, debes entender la distinción entre Kind y Type. El Kind es la categoría primitiva (un type MyInt int tiene un Kind de reflect.Int), mientras que el Type es la definición específica del tipo definido por el usuario. Usarás el Kind para decidir la lógica de tu algoritmo (si es un Slice, itero; si es un Struct, busco campos) y el Type para obtener metadatos como los struct tags.
Si intentas usar reflect de forma errónea, lo más probable es que tu programa compile sin problemas pero falle con un panic en tiempo de ejecución. La reflexión es potente pero peligrosa porque el compilador no puede ayudarte a verificar que un índice de un campo exista o que un mapa tenga la llave que buscas.
package main
import (
"fmt"
"reflect"
)
// Metadata representa etiquetas de configuración para un sistema de DB.
type Metadata struct {
ID string `db:"primary_key"`
}
// User es una estructura compleja para demostrar recursividad.
type User struct {
ID int `db:"id"`
Username string `db:"username"`
Roles []string `db:"roles"`
Extra map[string]string `db:"extra_info"`
Meta Metadata `db:"meta"`
}
// Inspect realiza una inspección profunda y recursiva de cualquier valor.
func Inspect(v any) {
val := reflect.ValueOf(v)
inspectRecursive(val, 0)
}
func inspectRecursive(v reflect.Value, depth int) {
// Indentación para visualización
indent := ""
for i := 0; i < depth; i++ {
indent += " "
}
// Obtenemos el Kind para saber qué tipo de dato es en runtime.
// Si es un puntero, navegamos hasta el valor real.
for v.Kind() == reflect.Ptr {
if v.IsNil() {
fmt.Printf("%s[nil pointer]\n", indent)
return
}
v = v.Elem()
}
t := v.Type()
switch v.Kind() {
case reflect.Struct:
fmt.Printf("%sStruct tipo %s:\n", indent, t.Name())
// Iteramos por los campos de la estructura.
for i := 0; i < v.NumField(); i++ {
fieldVal := v.Field(i)
fieldType := t.Field(i) // Información del tipo (tags, nombre)
// Usamos el tag 'db' si existe para ver la metadata.
dbTag := fieldType.Tag.Get("db")
if dbTag != "" {
dbTag = fmt.Sprintf(" (tag db: %s)", dbTag)
}
fmt.Printf("%s- Campo: %s%s\n", indent, fieldType.Name, dbTag)
// Llamada recursiva para estructuras anidadas.
inspectRecursive(fieldVal, depth+2)
}
case reflect.Slice, reflect.Array:
fmt.Printf("%sSlice/Array de longitud %d:\n", indent, v.Len())
for i := 0; i < v.Len(); i++ {
inspectRecursive(v.Index(i), depth+1)
}
case reflect.Map:
fmt.Printf("%sMap con %d elementos:\n", indent, v.Len())
// MapKeys nos da las llaves para poder acceder a los valores.
for _, key := range v.MapKeys() {
fmt.Printf("%s- Llave: %v\n", indent, key.Interface())
inspectRecursive(v.MapIndex(key), depth+1)
}
case reflect.String:
fmt.Printf("%sValor (string): %v\n", indent, v.String())
case reflect.Int:
fmt.Printf("%sValor (int): %v\n", indent, v.Int())
default:
// Para tipos básicos que no tratamos explícitamente.
fmt.Printf("%sValor: %v\n", indent, v.Interface())
}
}
func main() {
u := User{
ID: 101,
Username: "dev_senior",
Roles: []string{"admin", "editor"},
Extra: map[string]string{
"region": "us-east",
},
Meta: Metadata{
ID: "uuid-1234-5678",
},
}
Inspect(u)
}
Análisis del código
En el main, pasamos una instancia de User a Inspect. Observa cómo Inspect recibe un any (equivalente a interface{}), pero inmediatamente lo convierte en reflect.Value mediante reflect.ValueOf(v). Este paso es crucial: sin esto, no tenemos acceso a las capacidades de inspección.
Dentro de inspectRecursive, lo primero que hacemos es manejar los punteros. Si pasamos un puntero, v.Elem() nos permite “desreferenciarlo” para inspeccionar el valor real al que apunta. Esto es vital para que la recursividad funcione correctamente si tienes campos que son punteros a otros structs.
Cuando el Kind es reflect.Struct, utilizamos v.NumField() para saber cuántos campos tiene la estructura. Es importante entender que v.Field(i) nos devuelve un nuevo reflect.Value que representa el campo, mientras que t.Field(i) (donde t es v.Type()) nos devuelve la información de definición de ese campo. Usamos t.Field(i).Tag.Get("db") porque los tags viven en la definición del tipo, no en el valor en sí. Si intentaras buscar el tag directamente en v.Field(i), el programa no compilaría o no encontraría nada, porque los valores no tienen tags, los tipos sí.
Para el reflect.Slice, usamos v.Len() para la longitud e v.Index(i) para acceder a cada elemento. En el caso de reflect.Map, necesitamos primero obtener las llaves con v.MapKeys() y luego usar v.MapIndex(key) para obtener el valor asociado a esa llave.
El error frecuente
Un error clásico al trabajar con structs es intentar acceder o modificar campos privados (no exportados) mediante reflexión.
type persona struct {
nombre string // campo privado
Edad int // campo público
}
p := persona{nombre: "Ana", Edad: 30}
v := reflect.ValueOf(p)
// ESTO CAUSARÁ UN PANIC
fmt.Println(v.Field(0).Interface())
Aunque v.Field(0) te permite acceder al campo nombre, el método .Interface() (que devuelve un any) tiene una restricción de seguridad: no puede devolver valores de campos no exportados. Si intentas leer el valor de un campo privado para devolverlo como una interfaz, el runtime lanzará un panic. Siempre debes verificar si un campo es exportado si tu lógica depende de acceder a su valor mediante Interface().
N° 176