En Go, el paquete context es el mecanismo estándar para propagar señales de cancelación, tiempos de espera (deadlines) y valores a lo largo de los límites de una API y entre goroutines. Imagina que el context es un árbol: cada vez que usas una función como WithCancel o WithTimeout, estás creando un nodo hijo que depende de su padre. Si el padre se cancela o llega a su tiempo límite, la señal de cancelación se propaga automáticamente hacia abajo, “matando” a todos los descendientes.
Técnicamente, un context es una interfaz que contiene un canal Done(). Cuando el contexto se cancela, ese canal se cierra. Esto permite que las funciones de larga duración utilicen un bloque select para escuchar este canal y detener su ejecución de forma limpia.
Usamos context.Background() como el punto de partida absoluto, la raíz de todo nuestro árbol de ejecución; es el contexto que nunca se cancela y suele instanciarse en la función main o en el inicio de un proceso de larga vida. Por otro lado, context.TODO() es una herramienta semántica: lo usas cuando sabes que una función requiere un contexto, pero aún no tienes el objeto adecuado o estás en una fase de desarrollo donde el flujo de datos aún no está conectado. No es que uno sea mejor que otro, es que le dicen algo distinto a quien lee tu código.
Si implementas mal esta jerarquía —por ejemplo, ignorando el canal Done() en una goroutine pesada o no cancelando los contextos hijos— lo que rompes es la capacidad de tu sistema para liberar recursos. Terminarás con “goroutines zombies”: procesos que siguen consumiendo CPU y memoria en segundo plano porque nadie les avisó que el trabajo ya no es necesario.
package main
import (
"context"
"fmt"
"time"
)
// simulateDatabaseCall simula una consulta pesada a una base de datos.
func simulateDatabaseCall(ctx context.Context, query string) {
fmt.Printf("Iniciando consulta: %s\n", query)
// Creamos un canal para simular el trabajo de la DB
done := make(chan string)
go func() {
// Simulamos latencia de red/disco
time.Sleep(3 * time.Second)
done <- "Resultado de: " + query
}()
// El select es crucial: escuchamos el trabajo O la cancelación
select {
case res := <-done:
fmt.Println(res)
case <-ctx.Done():
// Si el contexto se cancela, salimos inmediatamente.
// ctx.Err() nos dice por qué se canceló (DeadlineExceeded o Canceled)
fmt.Printf("Consulta cancelada: %v\n", ctx.Err())
}
}
func main() {
// 1. El contexto raíz (el ancestro de todos)
rootCtx := context.Background()
// 2. Creamos un contexto hijo con un timeout de 2 segundos.
// Como la simulación tarda 3s, este contexto se cancelará antes.
childCtx, cancel := context.WithTimeout(rootCtx, 2*time.Second)
// Es vital llamar a cancel() para liberar recursos internos del contexto,
// incluso si el timeout ocurre de todos modos.
defer cancel()
// Ejecutamos la tarea con el contexto con tiempo límite
simulateDatabaseCall(childCtx, "SELECT * FROM usuarios")
// Esperamos un poco para observar que la goroutine de la simulación
// no bloquea la salida del programa principal de forma descontrolada.
time.Sleep(1 * time.Second)
}
En el ejemplo anterior, hemos establecido una jerarquía clara. Empezamos con rootCtx mediante context.Background(), que actúa como el ancla. Luego, context.WithTimeout crea un childCtx que es un descendiente directo de rootCtx.
Fíjate en la función simulateDatabaseCall: la clave de la eficiencia no es solo llamar a la función, sino cómo reacciona a la señal. Al usar el bloque select, la goroutine no se queda bloqueada esperando el canal done si el tiempo se agota. En cuanto el temporizador de 2 segundos de childCtx expira, el runtime de Go cierra el canal Done(), lo que activa el segundo caso del select. La variable ctx.Err() nos devuelve context.DeadlineExceeded, permitiéndonos saber que no fue una cancelación manual, sino que el tiempo se agotó.
El error frecuente
El error más común en sistemas de alta carga es el “leaking de contextos” (fuga de contextos). Ocurre cuando utilizas context.WithCancel o context.WithTimeout pero olvidas llamar a la función cancel que devuelve la función.
// ERROR: Fuga de memoria y recursos
func badFunction(ctx context.Context) {
// Si esta función se llama miles de veces por segundo y no llamamos a cancel(),
// creamos miles de objetos y timers que el Garbage Collector no puede
// limpiar eficientemente porque el contexto sigue "vivo" en la jerarquía.
childCtx, _ := context.WithCancel(ctx)
go doWork(childCtx)
// Olvidamos el defer cancel()
}
Aunque el proceso principal termine, los recursos internos asociados al timer o a la estructura del contexto hijo pueden permanecer en memoria hasta que el padre se cierre. En una API que procesa peticiones constantes, esto se traduce en un aumento progresivo y letal del uso de memoria.
N° 124