Context en Go: Cancelación, Timeouts y Valores de Request-Scope
En los posts anteriores exploramos las goroutines, channels y el select statement. Hoy vamos a descubrir una de las características más importantes de Go para escribir código concurrente robusto: el context package. Si vienes de Java, context es similar a CompletableFuture.cancel() o ExecutorService.shutdownNow(), pero mucho más poderoso e integrado.
¿Qué es Context?
El context package proporciona una forma estándar de manejar:
✅ Cancelación: Cancelar operaciones en cascada
✅ Timeouts: Limitar el tiempo de ejecución
✅ Deadlines: Tiempos límite absolutos
✅ Valores: Pasar datos de request-scope (user IDs, trace IDs, etc.)
Comparación: Java vs Go
| Aspecto | Java | Go |
| Cancelación | Future.cancel(), ExecutorService.shutdown() | context.WithCancel() |
| Timeouts | Future.get(timeout, unit) | context.WithTimeout() |
| Propagación | Manual (pasando flags) | Automática (pasando context) |
| Valores | ThreadLocal (problemático) | Context values (seguro) |
| Integración | Librerías externas | Estándar del lenguaje |
Context Básico: Background y TODO
Go proporciona dos context base:
// context.Background() - Context raíz, nunca se cancela
ctx := context.Background()
// context.TODO() - Placeholder cuando no estás seguro qué usar
ctx := context.TODO()
Cuándo usar cada uno:
Background(): Context principal de tu aplicación (main, handlers HTTP, etc.)TODO(): Cuando no estás seguro qué context usar (útil durante desarrollo)
Context con Timeout
Uno de los casos de uso más comunes es limitar el tiempo de ejecución de una operación:
// Crear context con timeout de 2 segundos
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // ← Siempre llama cancel() para liberar recursos
// Simular operación que puede tardar
done := make(chan bool)
go func() {
time.Sleep(3 * time.Second) // Tarda más que el timeout
done <- true
}()
select {
case <-done:
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Printf("Operation cancelled: %v\n", ctx.Err())
// ctx.Err() retorna context.DeadlineExceeded
}
Características importantes:
✅
cancel()debe llamarse siempre para liberar recursos✅ Usa
defer cancel()para asegurar que se llame✅
ctx.Done()retorna un channel que se cierra cuando el context se cancela✅
ctx.Err()retorna el error (context.DeadlineExceededocontext.Canceled)
Verificar si un Context está Cancelado
if ctx.Err() != nil {
return ctx.Err() // Ya está cancelado
}
Context con Cancelación Manual
A veces necesitas cancelar una operación manualmente, no por timeout:
ctx, cancel := context.WithCancel(context.Background())
// Goroutine que hace trabajo
go func() {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Printf("Worker cancelled: %v\n", ctx.Err())
return
default:
fmt.Printf("Working... %d\n", i)
time.Sleep(200 * time.Millisecond)
}
}
}()
// Cancelar después de 1 segundo
time.Sleep(1 * time.Second)
fmt.Println("Cancelling context...")
cancel() // ← Cancela el context y todas sus operaciones
time.Sleep(500 * time.Millisecond)
Casos de uso:
✅ Cancelar cuando el usuario presiona "Cancelar"
✅ Cancelar cuando ocurre un error y no necesitas continuar
✅ Cancelar operaciones en cascada cuando una falla
Context con Deadline
Similar a timeout, pero con un tiempo absoluto:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Printf("Deadline exceeded: %v\n", ctx.Err())
// ctx.Err() retorna context.DeadlineExceeded
}
Diferencia entre Timeout y Deadline:
WithTimeout: Tiempo relativo desde ahoraWithDeadline: Tiempo absoluto específico
Cuándo usar cada uno:
WithTimeout: "Esta operación debe completarse en 5 segundos"WithDeadline: "Esta operación debe completarse antes de las 3:00 PM"
Context con Valores
Los context pueden llevar valores que se propagan a través de la cadena de llamadas. Esto es útil para request IDs, user IDs, trace IDs, etc.
// Crear context con valores
ctx := context.WithValue(context.Background(), "userID", 123)
ctx = context.WithValue(ctx, "requestID", "req-456")
ctx = context.WithValue(ctx, "traceID", "trace-789")
// Función que usa los valores
processRequest := func(ctx context.Context) {
userID := ctx.Value("userID").(int)
requestID := ctx.Value("requestID").(string)
traceID := ctx.Value("traceID").(string)
fmt.Printf("Processing request:\n")
fmt.Printf(" User ID: %d\n", userID)
fmt.Printf(" Request ID: %s\n", requestID)
fmt.Printf(" Trace ID: %s\n", traceID)
}
processRequest(ctx)
⚠️ Mejores Prácticas para Valores
1. Usa tipos específicos para las keys:
// ✅ BIEN: Tipo específico para evitar colisiones
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
ctx := context.WithValue(context.Background(), userIDKey, 123)
userID := ctx.Value(userIDKey).(int)
// ❌ MAL: Strings pueden colisionar
ctx := context.WithValue(context.Background(), "userID", 123)
2. Solo usa valores para datos de request-scope:
// ✅ BIEN: Request-scope data
ctx = context.WithValue(ctx, "userID", userID)
ctx = context.WithValue(ctx, "requestID", requestID)
// ❌ MAL: No uses context para pasar parámetros de función
func processUser(ctx context.Context, userID int) { // ← userID como parámetro
// ...
}
3. Los valores deben ser inmutables:
// ✅ BIEN: Valores primitivos o inmutables
ctx = context.WithValue(ctx, "userID", 123)
ctx = context.WithValue(ctx, "requestID", "req-123")
// ❌ MAL: No uses estructuras mutables
type Config struct {
APIKey string
}
ctx = context.WithValue(ctx, "config", &Config{APIKey: "secret"}) // ← Puede mutarse
Propagando Context en Funciones
Una de las reglas más importantes en Go: siempre pasa context.Context como primer parámetro en funciones que pueden cancelarse o necesitan valores del context.
Convención de Nombres
// ✅ BIEN: Context como primer parámetro
func longRunningOperation(ctx context.Context, duration time.Duration) error {
// ...
}
func fetchData(ctx context.Context, url string) ([]byte, error) {
// ...
}
func processUser(ctx context.Context, userID int) error {
// ...
}
Ejemplo: Operación con Checks Periódicos
func longRunningOperation(ctx context.Context, duration time.Duration) error {
fmt.Printf("Starting operation (will take %v)...\n", duration)
// Verificar si ya está cancelado
if ctx.Err() != nil {
return ctx.Err()
}
// Simular trabajo con checks periódicos de cancelación
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
start := time.Now()
for {
select {
case <-ctx.Done():
fmt.Printf("Operation cancelled after %v\n", time.Since(start))
return ctx.Err()
case <-ticker.C:
elapsed := time.Since(start)
fmt.Printf(" Still working... (%v elapsed)\n", elapsed)
if elapsed >= duration {
fmt.Printf("Operation completed in %v\n", elapsed)
return nil
}
}
}
}
// Uso
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := longRunningOperation(ctx, 3*time.Second); err != nil {
fmt.Printf("Error: %v\n", err)
}
Puntos clave:
✅ Verifica
ctx.Done()periódicamente en loops largos✅ Retorna
ctx.Err()cuando detectas cancelación✅ Usa
selectpara no bloquear innecesariamente
Context en HTTP Requests
El paquete net/http integra context directamente. Cada request tiene un context que puedes usar:
type HTTPClient struct{}
func (c *HTTPClient) Get(ctx context.Context, url string) (string, error) {
// Simular request HTTP con cancelación
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(1 * time.Second):
return fmt.Sprintf("Response from %s", url), nil
}
}
// Uso
client := &HTTPClient{}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
result, err := client.Get(ctx, "https://api.example.com/data")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
} else {
fmt.Printf("Success: %s\n", result)
}
Context en HTTP Handlers
En handlers HTTP, el context viene del request:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // ← Context del request
// Agregar valores al context
ctx = context.WithValue(ctx, "userID", getUserID(r))
// Pasar a funciones que necesitan el context
if err := processRequest(ctx); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Success"))
}
Características del HTTP Context:
✅ Se cancela automáticamente cuando el cliente cierra la conexión
✅ Puedes agregar valores (user ID, request ID, etc.)
✅ Puedes crear timeouts específicos para operaciones dentro del handler
Context en Operaciones de Base de Datos
La mayoría de drivers de base de datos en Go aceptan context:
type Database struct{}
func (db *Database) Query(ctx context.Context, query string) ([]string, error) {
// Simular query que puede tardar
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(2 * time.Second):
return []string{"result1", "result2", "result3"}, nil
}
}
// Uso
db := &Database{}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
results, err := db.Query(ctx, "SELECT * FROM users")
if err != nil {
fmt.Printf("Query failed: %v\n", err)
} else {
fmt.Printf("Results: %v\n", results)
}
Ejemplo Real con database/sql
import (
"context"
"database/sql"
"time"
)
func getUser(ctx context.Context, db *sql.DB, userID int) (*User, error) {
// Crear context con timeout para la query
queryCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var user User
err := db.QueryRowContext(queryCtx,
"SELECT id, name, email FROM users WHERE id = $1", userID).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
Context Chaining: Encadenar Contexts
Puedes encadenar contexts para agregar más información o timeouts:
// Context base con valores
baseCtx := context.WithValue(context.Background(), "userID", 123)
// Agregar timeout
timeoutCtx, cancel1 := context.WithTimeout(baseCtx, 2*time.Second)
defer cancel1()
// Agregar más valores
finalCtx := context.WithValue(timeoutCtx, "requestID", "req-999")
// Usar el context final
fmt.Printf("User ID: %d\n", finalCtx.Value("userID"))
fmt.Printf("Request ID: %s\n", finalCtx.Value("requestID"))
// El timeout también está presente
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-finalCtx.Done():
fmt.Printf("Timeout: %v\n", finalCtx.Err())
}
Características del chaining:
✅ Los valores se propagan hacia abajo
✅ Los timeouts/deadlines se heredan (el más corto gana)
✅ La cancelación se propaga hacia abajo
Ejemplo Práctico: Servicio con Múltiples Operaciones
Un ejemplo completo de cómo usar context en un servicio real:
type Service struct {
db *Database
client *HTTPClient
}
func NewService() *Service {
return &Service{
db: &Database{},
client: &HTTPClient{},
}
}
func (s *Service) ProcessData(ctx context.Context, userID int) error {
// Agregar userID al context
ctx = context.WithValue(ctx, "userID", userID)
// Hacer múltiples operaciones con el mismo context
// Si alguna falla o se cancela, todas se cancelan
// 1. Query database
results, err := s.db.Query(ctx, "SELECT * FROM data")
if err != nil {
return fmt.Errorf("database query failed: %w", err)
}
// 2. Fetch external data
externalData, err := s.client.Get(ctx, "https://api.example.com/data")
if err != nil {
return fmt.Errorf("external API call failed: %w", err)
}
fmt.Printf("Processed data: %v, External: %s\n", results, externalData)
return nil
}
// Uso
service := NewService()
// Context con timeout para toda la operación
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := service.ProcessData(ctx, 123); err != nil {
fmt.Printf("Error: %v\n", err)
}
Ventajas:
✅ Si la query de DB falla, el context se cancela y la llamada HTTP también puede cancelarse
✅ Si el timeout se alcanza, ambas operaciones se cancelan
✅ Los valores (userID) están disponibles en todas las funciones
Context en Worker Pools
Usar context para cancelar workers cuando sea necesario:
func workerPoolWithContext(ctx context.Context, numWorkers int) {
jobs := make(chan Task, 10)
results := make(chan Result, 10)
// Workers con cancelación
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Cancelled\n", workerID)
return
case task, ok := <-jobs:
if !ok {
return
}
// Procesar tarea
result := processTask(ctx, task)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}(w)
}
// Enviar trabajos con cancelación
go func() {
defer close(jobs)
for i := 1; i <= 100; i++ {
select {
case jobs <- Task{ID: i}:
case <-ctx.Done():
return
}
}
}()
// Cerrar results cuando terminen
go func() {
wg.Wait()
close(results)
}()
// Recibir resultados
for result := range results {
fmt.Printf("Result: %v\n", result)
}
}
Mejores Prácticas
1. Siempre Pasa Context como Primer Parámetro
// ✅ BIEN
func processData(ctx context.Context, data []byte) error {
// ...
}
// ❌ MAL
func processData(data []byte, ctx context.Context) error {
// ...
}
2. Siempre Llama cancel() con defer
// ✅ BIEN
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // ← Siempre libera recursos
// ❌ MAL
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// Olvidar cancel() puede causar memory leaks
3. No Almacenes Context en Structs
// ❌ MAL: Context en struct
type Service struct {
ctx context.Context // ← No hagas esto
}
// ✅ BIEN: Pasa context como parámetro
type Service struct {
db *Database
}
func (s *Service) Process(ctx context.Context, data Data) error {
// ...
}
Razón: Los context son específicos de request y no deben compartirse entre requests.
4. Usa Tipos Específicos para Context Keys
// ✅ BIEN
type contextKey string
const userIDKey contextKey = "userID"
ctx := context.WithValue(ctx, userIDKey, 123)
userID := ctx.Value(userIDKey).(int)
// ❌ MAL: Strings pueden colisionar
ctx := context.WithValue(ctx, "userID", 123)
5. Verifica Cancelación en Loops Largos
// ✅ BIEN: Verificar cancelación periódicamente
for i := 0; i < 1000000; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Procesar
}
}
// ❌ MAL: Loop largo sin verificar cancelación
for i := 0; i < 1000000; i++ {
// Procesar sin verificar ctx.Done()
}
6. No Pases nil Context
// ❌ MAL
func process(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
// ...
}
// ✅ BIEN: Si es opcional, usa context.Background() como default
func process(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
// O mejor: siempre requiere context
}
Errores Comunes
❌ Error 1: Olvidar Llamar cancel()
// ❌ MAL: Memory leak
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// Olvidamos cancel()
// ✅ BIEN: Siempre usar defer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
❌ Error 2: Almacenar Context en Structs
// ❌ MAL: Context compartido entre requests
type Service struct {
ctx context.Context
}
func NewService(ctx context.Context) *Service {
return &Service{ctx: ctx} // ← No hagas esto
}
// ✅ BIEN: Pasar context como parámetro
type Service struct {
db *Database
}
func (s *Service) Process(ctx context.Context, data Data) error {
// ...
}
❌ Error 3: No Verificar Cancelación en Loops
// ❌ MAL: No puede cancelarse
func processLargeDataset(ctx context.Context, data []Item) {
for _, item := range data {
processItem(item) // ← No verifica ctx.Done()
}
}
// ✅ BIEN: Verificar periódicamente
func processLargeDataset(ctx context.Context, data []Item) error {
for i, item := range data {
select {
case <-ctx.Done():
return ctx.Err()
default:
processItem(item)
}
// O verificar cada N iteraciones
if i%1000 == 0 {
if ctx.Err() != nil {
return ctx.Err()
}
}
}
return nil
}
❌ Error 4: Usar Context Values para Parámetros de Función
// ❌ MAL: Context no es para pasar parámetros
func getUser(ctx context.Context) (*User, error) {
userID := ctx.Value("userID").(int) // ← No hagas esto
// ...
}
// ✅ BIEN: Parámetros explícitos
func getUser(ctx context.Context, userID int) (*User, error) {
// ...
}
Regla: Context values son para datos de request-scope (trace IDs, request IDs), no para parámetros de función.
❌ Error 5: Crear Context sin Background o TODO
// ❌ MAL: No crear context desde nil
var ctx context.Context // nil
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // ← Panic!
// ✅ BIEN: Siempre empezar con Background o TODO
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Comparación: Java vs Go
Java: Cancelación Manual
// Java
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> {
// Trabajo largo
return "result";
});
// Cancelar después de timeout
try {
String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
}
// Problemas:
// - No hay propagación automática
// - No hay valores de request-scope
// - Requiere manejo manual de excepciones
Go: Context
// Go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := longOperation(ctx)
if err != nil {
// Manejar error (puede ser cancelación o timeout)
}
// Ventajas:
// - Propagación automática
// - Valores de request-scope
// - Integrado en el lenguaje
// - Funciona con todas las operaciones
Context y Select: Combinación Poderosa
El context y el select trabajan perfectamente juntos:
func processWithMultipleSources(ctx context.Context) error {
ch1 := make(chan Result)
ch2 := make(chan Result)
go fetchFromSource1(ctx, ch1)
go fetchFromSource2(ctx, ch2)
select {
case result := <-ch1:
return processResult(result)
case result := <-ch2:
return processResult(result)
case <-ctx.Done():
return ctx.Err() // Timeout o cancelación
}
}
Conclusiones
El context package es esencial para escribir código concurrente robusto en Go:
✅ Cancelación en cascada: Cancela operaciones hijas automáticamente
✅ Timeouts y deadlines: Limita el tiempo de ejecución de forma elegante
✅ Valores de request-scope: Pasa datos de forma segura sin ThreadLocal
✅ Integración estándar: Funciona con HTTP, databases, y todas las librerías estándar
✅ Propagación automática: Pasa el context y todo funciona
✅ Type-safe: Previene errores en tiempo de compilación
Si vienes de Java, el context puede parecer diferente al principio, pero una vez que entiendas cómo funciona y lo uses consistentemente, verás cómo simplifica enormemente el manejo de cancelación, timeouts y valores de request-scope en aplicaciones concurrentes.
Próximos Pasos
En el siguiente post exploraremos los patrones de sincronización en Go usando el paquete sync, incluyendo WaitGroup, Mutex, RWMutex, y otros primitivos de sincronización que complementan perfectamente las goroutines, channels y context.
¿Has usado context en tus proyectos de Go? ¿Qué patrones te han resultado más útiles? Comparte tus experiencias y casos de uso en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.




