El Paquete context de Go en 2026: Cancelación, Timeouts y Preguntas de Entrevista

Guía completa del paquete context de Go: gestión de cancelación, timeouts y deadlines. Incluye preguntas frecuentes de entrevistas técnicas y mejores prácticas de producción.

El paquete context de Go - Cancelación y Timeouts

El paquete context proporciona el mecanismo estándar en Go para transportar deadlines, señales de cancelación y valores asociados a solicitudes a través de las fronteras de API. Cada aplicación Go en producción utiliza context: los handlers HTTP, consultas a bases de datos, llamadas gRPC y workers en segundo plano dependen de este paquete para el apagado elegante y la gestión de timeouts.

Esencial en Entrevistas

El context representa uno de los temas más frecuentemente consultados en entrevistas de Go. Los entrevistadores esperan que los candidatos expliquen la propagación del context, demuestren un manejo correcto de la cancelación y eviten errores comunes como almacenar el context en estructuras.

La Interfaz Context y sus Cuatro Métodos

La interfaz Context define cuatro métodos que toda implementación debe satisfacer. Comprender estos métodos constituye la base para un uso efectivo del context.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() retorna un channel de solo lectura. Cuando este channel se cierra, cualquier goroutine escuchándolo recibe la señal inmediatamente. El patrón <-ctx.Done() bloquea hasta que ocurre la cancelación. Err() explica la razón: ya sea context.Canceled cuando se cancela explícitamente, o context.DeadlineExceeded cuando un timeout o deadline ha expirado.

Creación de Contexts con Background y TODO

Dos funciones crean contexts raíz: context.Background() y context.TODO(). Ambas retornan contexts no nulos y vacíos que nunca se cancelan.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() sirve como padre para las solicitudes entrantes, funciones main y código de inicialización. TODO() actúa como un placeholder durante el refactoring cuando la fuente correcta del context aún no está determinada. Las herramientas de análisis estático pueden señalar el uso de TODO() para seguimiento posterior.

Cancelación con WithCancel y WithCancelCause

La cancelación manual permite que las goroutines padres señalen a las hijas que el trabajo debe detenerse. Este patrón aparece en pools de workers, tareas en segundo plano e implementaciones de apagado elegante.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, introducido en Go 1.20, proporciona información de error más rica que el simple WithCancel. La función context.Cause() recupera el error pasado a la función cancel, facilitando la depuración en sistemas complejos.

¿Listo para aprobar tus entrevistas de Go?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Timeouts y Deadlines para el Procesamiento de Solicitudes

Los handlers HTTP, llamadas a bases de datos y solicitudes a APIs externas siempre deben tener timeouts. El paquete context ofrece WithTimeout y WithDeadline para este propósito. Ambos crean contexts que se cancelan automáticamente cuando el tiempo expira.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

El patrón defer cancel() garantiza que los recursos se liberen incluso cuando la operación termina antes del timeout. Omitir esta llamada causa una fuga de recursos: la goroutine del timer del context permanece activa hasta que el timeout expira.

Valores Asociados a Solicitudes con WithValue

WithValue adjunta datos asociados a una solicitud a un context. Los casos de uso comunes incluyen identificadores de solicitud, tokens de autenticación y spans de trazado. La documentación oficial enfatiza el uso de tipos personalizados como claves para evitar colisiones.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Los valores del context forman una cadena inmutable: cada llamada a WithValue crea un nuevo context que envuelve al padre. Las búsquedas recorren esta cadena, haciendo que los valores profundamente anidados sean más lentos de acceder. Solo se deben almacenar datos asociados a solicitudes, no configuración de la aplicación ni parámetros opcionales.

Utilidades WithoutCancel y AfterFunc

Go 1.21 agregó WithoutCancel para operaciones que deben completarse independientemente de la cancelación del padre, como tareas de limpieza. AfterFunc programa un callback cuando ocurre la cancelación del context.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel preserva los valores del context padre pero ignora su señal de cancelación. Este patrón es adecuado para logging, emisión de métricas y commits de base de datos que deben tener éxito independientemente del resultado de la solicitud.

Preguntas de Entrevista y Respuestas

La preparación para una entrevista técnica de Go requiere comprender los mecanismos internos del context y las mejores prácticas. Estas preguntas aparecen frecuentemente en entrevistas de empresas que utilizan Go para servicios backend. Para más preparación de entrevistas Go, consulta el módulo Preguntas de entrevista sobre Context Package.

¿Por qué el context debe ser el primer parámetro?

El equipo de Go estableció esta convención para hacer visible y consistente la propagación del context. Colocar el context primero señala a los lectores que la función respeta la cancelación. La biblioteca estándar sigue este patrón: http.Request.Context(), database/sql.QueryContext() y grpc.UnaryInterceptor esperan el context como primer argumento.

¿Qué sucede si se almacena el context en una estructura?

Almacenar el context rompe el modelo de ciclo de vida de las solicitudes. Una estructura puede sobrevivir a la solicitud para la que fue creada, causando operaciones que usan señales de cancelación obsoletas o pierden nuevos deadlines. La documentación del context advierte explícitamente contra este patrón.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

¿Cómo manejar el context en goroutines?

Las goroutines deben verificar ctx.Done() en su bucle principal o instrucción select. Ignorar el channel done crea fugas de goroutines: la función padre retorna, pero la goroutine continúa consumiendo recursos.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

¿Cuándo usar context.TODO()?

Se debe usar TODO() durante el refactoring incremental al agregar soporte de context a código legacy. Marca ubicaciones que necesitan propagación correcta del context. El código de producción eventualmente debe reemplazar todas las llamadas TODO() con contexts reales provenientes de handlers de solicitudes o inicialización de la aplicación. Para más información sobre patrones de concurrencia en Go, consulta Go Concurrency: Goroutines and Channels.

Errores Comunes a Evitar

El uso incorrecto del context conduce a bugs sutiles que se manifiestan bajo carga o durante secuencias de apagado. Reconocer estos patrones previene incidentes en producción.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Puntos Clave para Usar Go Context en Producción

  • Pasar el context como primer parámetro de las funciones, nunca almacenarlo en estructuras
  • Siempre usar defer cancel() después de WithTimeout, WithDeadline o WithCancel para prevenir fugas de recursos
  • Usar WithCancelCause y context.Cause() para mejor depuración de errores en cadenas de cancelación complejas
  • Verificar <-ctx.Done() en bucles de goroutines para permitir apagado elegante y prevenir fugas
  • Usar tipos de claves personalizados para WithValue para evitar colisiones entre paquetes
  • Aplicar WithoutCancel para operaciones de limpieza que deben completarse independientemente de la cancelación del padre
  • Preferir context.Background() para inicialización de la aplicación y context.TODO() solo durante refactoring
Reto diario

¿Sabrías detectar el bug en Go?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 29 de agosto de 2026

Etiquetas

#go
#context
#concurrency
#interview

Compartir

Artículos relacionados