Uso correcto de context.Value en Go

context.Value es un mecanismo diseñado para transportar información a través de las capas de una llamada en Go. No es un mapa de variables globales, aunque es común ver este error en código mal diseñado. Técnicamente, cada vez que llamas a context.WithValue, estás creando un nuevo nodo en un árbol de contextos; el valor se busca de forma ascendente, desde el contexto hijo hasta la raíz, hasta encontrar la clave buscada.

Para entenderlo bien, veamos sus reglas de oro: es un mecanismo de transporte para metadatos request-scoped (vinculados al ciclo de vida de una petición específica) y no una herramienta de inyección de dependencias. ¿Qué es exactamente? Es un contenedor de datos adjuntos a un árbol de ejecución. ¿Por qué funciona así? Para permitir que la información viaje por múltiples capas de un sistema (middleware, servicios, repositorios) sin tener que modificar la firma de cada función en el camino. ¿Cuándo deberías usarlo? Únicamente cuando necesites propagar datos de trazabilidad (como un TraceID para logging) o información de contexto de seguridad (como un UserID extraído de un token) que no afecte la lógica de negocio primaria. ¿Qué rompe si lo usas mal? Si intentas pasar una conexión a una base de datos o un logger a través del contexto, estarás ocultando dependencias críticas. Esto hace que tus funciones sean “mentirosas” (su firma no refleja lo que realmente necesitan para operar), vuelve los tests unitarios un infierno de configuración y te despoja de la seguridad de tipos, ya que ctx.Value devuelve un any, obligándote a realizar aserciones de tipo manuales que pueden fallar en tiempo de ejecución.

package main

import (
	"context"
	"fmt"
)

// Definir un tipo privado para la clave es vital para evitar colisiones.
// Si usaras un string directamente, otra librería podría usar el mismo 
// string y sobrescribir tu valor.
type ctxKey int

const (
	traceIDKey ctxKey = iota
	requestIDKey
)

func main() {
	// Simulamos el inicio de una petición en un servidor HTTP.
	// El contexto raíz es el origen de la cadena.
	ctx := context.Background()

	// Inyectamos metadatos que solo interesan para esta petición.
	ctx = context.WithValue(ctx, traceIDKey, "req-550e8400-e29b")
	ctx = context.WithValue(ctx, requestIDKey, "abc-123")

	handleRequest(ctx)
}

func handleRequest(ctx context.Context) {
	fmt.Println("Iniciando proceso de negocio...")
	// La lógica de negocio es agnóstica a cómo se obtuvieron los IDs.
	doDatabaseWork(ctx)
}

func doDatabaseWork(ctx context.Context) {
	// Extraemos el valor usando la clave personalizada.
	// Siempre debemos usar la aserción de tipo con el patrón "comma ok".
	traceID, ok := ctx.Value(traceIDKey).(string)
	if !ok {
		// Si la clave no existe o no es un string, usamos un valor por defecto
		// para no romper el flujo, pero permitimos que el programa continúe.
		traceID = "unknown"
	}

	fmt.Printf("[LOG] [%s] Ejecutando operación en la base de datos...\n", traceID)
}

Desglose del ejemplo

En el código anterior, el uso de type ctxKey int es la decisión de diseño más importante. Al definir un tipo propio para la clave, garantizamos que solo nuestro paquete pueda acceder a esos valores mediante esa clave específica, evitando que otras librerías con las que dependamos accidentalmente pisen nuestros datos si usan el mismo nombre de clave.

En main, vemos cómo context.WithValue no modifica el contexto original, sino que retorna un nuevo contexto que envuelve al anterior. Esto mantiene la inmutabilidad. Cuando ejecutamos doDatabaseWork, llamamos a ctx.Value(traceIDKey). El runtime de Go recorre la jerarquía de contextos hacia arriba hasta encontrar nuestra constante traceIDKey.

Fíjate en la aserción traceID, ok := ctx.Value(traceIDKey).(string). Dado que ctx.Value devuelve un any (interface{}), el compilador no sabe qué tipo de dato hay dentro. Si intentamos forzar el tipo sin la comprobación ok y el valor es nil o un tipo distinto, el programa lanzará un panic. En producción, siempre debes tratar el contenido de un contexto como algo que podría no estar presente.

El error frecuente

El error más común es tratar el context.Context como un contenedor de dependencias de infraestructura.

// MAL: Usar contexto para pasar la base de datos
func Setup() {
    db := &sql.DB{}
    // Esto es un anti-patrón: la dependencia está oculta
    ctx := context.WithValue(context.Background(), "db", db)
    Execute(ctx)
}

func Execute(ctx context.Context) {
    // Tienes que adivinar qué hay dentro y hacer type assertion
    db := ctx.Value("db").(*sql.DB) // Peligro de panic si la clave es incorrecta
    db.Query(...)
}

Si una función necesita una base de datos para operar, esa base de datos debe ser un parámetro explícito o un campo de la estructura, nunca un valor oculto en un contexto.

127

Dejar un comentario

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

Scroll al inicio