Struct Tags: Metadatos y reflexión en Go

Las struct tags son cadenas de texto literales, delimitadas por backticks (`), que se colocan inmediatamente después de la declaración de un campo en una estructura. No son instrucciones para el compilador, sino metadatos que residen en el binario y que el paquete reflect utiliza para inspeccionar la estructura en tiempo de ejecución. Las usas cuando necesitas definir cómo debe interpretarse un campo al interactuar con el mundo exterior, como al serializar a JSON, mapear campos de una base de datos SQL o validar la entrada de un formulario HTTP. Si las defines mal —por ejemplo, omitiendo los dos puntos o las comillas internas— el programa compilará sin problemas, pero las librerías simplemente ignorarán la etiqueta, lo que resultará en campos que aparecen con nombres inesperados o vacíos de forma totalmente silenciosa.

package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

// User representa un modelo de dominio.
// Usamos tags para:
// 1. Mapear nombres de campos en JSON.
// 2. Definir nombres de columnas en una base de datos (simulado).
// 3. Ocultar campos sensibles.
// 4. Indicar que un campo puede omitirse si está vacío.
type User struct {
	ID        int    `json:"id" db:"user_id"`
	Username  string `json:"username"`
	Email     string `json:"email,omitempty"`
	Password  string `json:"-"` // El guion "-" indica que debe ignorarse en JSON
	IsAdmin   bool   `db:"is_admin"`
}

func main() {
	u := User{
		ID:       101,
		Username: "gopher_pro",
		Email:    "", // Al estar vacío, omitempty hará que no aparezca en el JSON
		Password: "super-secret-password",
		IsAdmin:  true,
	}

	// 1. Demostración de serialización JSON
	jsonData, _ := json.MarshalIndent(u, "", "  ")
	fmt.Println("--- JSON Output (notar omitempty y el campo oculto) ---")
	fmt.Println(string(jsonData))

	// 2. Demostración de lectura de tags personalizados mediante reflexión
	fmt.Println("\n--- Lectura de metadatos de DB (vía reflect) ---")
	printDBTags(u)
}

// printDBTags utiliza reflexión para inspeccionar las etiquetas `db`
func printDBTags(s interface{}) {
	val := reflect.TypeOf(s)

	// Si pasamos un puntero, obtenemos el tipo al que apunta
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}

	for i := 0; i < val.NumField(); i++ {
		field := val.Field(i)
		// Buscamos específicamente la etiqueta "db"
		dbTag := field.Tag.Get("db")
		if dbTag != "" {
			fmt.Printf("Campo: %s | Columna DB: %s\n", field.Name, dbTag)
		}
	}
}

En el código anterior, fíjate cómo User actúa como nuestro modelo central. Cuando ejecutamos json.MarshalIndent, el paquete encoding/json no mira el nombre de la variable en Go (como Username), sino que busca la etiqueta json:"username". Si el campo Email tiene un valor de string vacío, la etiqueta omitempty le indica al encoder que debe omitir esa clave por completo del objeto resultante, evitando enviar basura innecesaria por la red.

La etiqueta json:"-" en el campo Password es crucial para la seguridad: le dice al serializador que, aunque el campo existe en memoria, nunca debe ser incluido en la salida JSON. Por otro lado, hemos definido etiquetas db:"..." que no son estándar de Go, sino que están pensadas para ser consumidas por un driver de base de datos o un ORM. Para leer estas etiquetas por nuestra cuenta, utilizamos reflect.TypeOf(s).Field(i).Tag.Get("db"). Es importante notar que reflect.TypeOf nos devuelve la información de tipo (metadatos), mientras que reflect.ValueOf nos daría acceso a los valores reales contenidos en la instancia.

El bucle for en printDBTags recorre cada campo de la estructura. La función field.Tag.Get("db") es la forma segura de extraer el valor de una etiqueta específica; si la etiqueta no existe, simplemente devuelve un string vacío, lo que nos permite filtrar campos que no tienen metadatos de base de datos.

El error frecuente

type BadStruct struct {
    // ERROR: Falta el ":" y las comillas. 
    // El encoder JSON ignorará esto y usará "Name" por defecto.
    Name string `json:name` 

    // ERROR: Espacio incorrecto antes de los dos puntos.
    // Esto suele causar que reflect.Get devuelva una cadena vacía.
    Age int `age: "25"` 
}

Si escribes json:name en lugar de json:"name", el compilador no te dará error, pero encoding/json no encontrará la clave json y usará el nombre del campo tal cual está en Go. Esto es especialmente peligroso en APIs donde esperas un formato snake_case y terminas enviando PascalCase, rompiendo la compatibilidad con tus clientes.

61

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio