Context, cancelación y timeouts

Avanzado
Go
Go
Actualizado: 03/04/2026

¿Por qué necesitamos context?

Imagina un servidor HTTP que recibe miles de peticiones simultáneas. Cada petición puede desencadenar consultas a la base de datos, llamadas a servicios externos y trabajo en goroutines. Si el cliente cancela la petición (cierra el navegador, timeout de red), el servidor debería:

Context en Go: cancelación, timeouts y deadlines

  1. Detener todas las operaciones asociadas a esa petición
  2. Liberar recursos (conexiones de BD, goroutines, etc.)
  3. Propagar la señal de cancelación por toda la cadena de llamadas

El paquete context estandariza este patrón en Go.

La interfaz context.Context

type Context interface {
    // Devuelve el tiempo de expiración, si existe
    Deadline() (deadline time.Time, ok bool)

    // Canal cerrado cuando el contexto es cancelado o expira
    Done() <-chan struct{}

    // Razón de la cancelación: context.Canceled o context.DeadlineExceeded
    Err() error

    // Recupera un valor asociado por clave
    Value(key any) any
}

Contextos raíz

Los contextos forman un árbol. Todo árbol parte de uno de los dos contextos raíz:

// Context vacío, nunca se cancela — úsalo en main() y tests de nivel superior
ctx := context.Background()

// Placeholder para código que todavía no usa context — documentación de intención
ctx := context.TODO()

context.WithCancel: cancelación manual

func procesarPedido(idPedido int) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // SIEMPRE llamar a cancel, incluso si la operación tiene éxito

    // Lanzar operaciones en goroutines
    errCh := make(chan error, 2)
    go func() { errCh <- reservarStock(ctx, idPedido) }()
    go func() { errCh <- cobrarPago(ctx, idPedido) }()

    // Esperar la primera respuesta
    if err := <-errCh; err != nil {
        cancel() // cancelar la otra goroutine
        log.Printf("Error al procesar pedido %d: %v", idPedido, err)
        return
    }
    <-errCh // esperar la segunda
    log.Printf("Pedido %d procesado", idPedido)
}

context.WithTimeout: tiempo límite relativo

Ideal para limitar peticiones HTTP salientes, consultas a base de datos o cualquier operación con duración máxima:

func consultarAPIExterna(url string) ([]byte, error) {
    // Máximo 5 segundos para obtener respuesta
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("la API no respondió en 5 segundos")
        }
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

context.WithDeadline: tiempo límite absoluto

func procesarAntesDeMedianoche(datos []string) error {
    // Calcular cuánto tiempo queda hasta medianoche
    ahora := time.Now()
    medianoche := time.Date(ahora.Year(), ahora.Month(), ahora.Day()+1,
        0, 0, 0, 0, ahora.Location())

    ctx, cancel := context.WithDeadline(context.Background(), medianoche)
    defer cancel()

    for _, dato := range datos {
        select {
        case <-ctx.Done():
            return fmt.Errorf("procesamiento interrumpido: %w", ctx.Err())
        default:
            procesar(ctx, dato)
        }
    }
    return nil
}

Convención: context.Context como primer parámetro

El contexto siempre debe ser el primer parámetro y nunca debe almacenarse en structs:

// CORRECTO
func ObtenerUsuario(ctx context.Context, id int) (*Usuario, error) {
    return db.QueryRowContext(ctx, "SELECT * FROM usuarios WHERE id = $1", id).Scan(...)
}

// INCORRECTO — no almacenar en structs
type Servicio struct {
    ctx context.Context // ❌ no hacer esto
}

Propagar valores con context.WithValue

Úsalo solo para datos transversales como IDs de solicitud o credenciales de autenticación, no como sustituto de parámetros de función:

type clave string
const (
    claveIDSolicitud clave = "id_solicitud"
    claveUsuario     clave = "usuario"
)

// Middleware HTTP que inyecta el ID de solicitud
func middlewareIDSolicitud(siguiente http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := uuid.NewString()
        ctx := context.WithValue(r.Context(), claveIDSolicitud, id)
        w.Header().Set("X-Request-ID", id)
        siguiente.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Recuperar el valor más adelante en la cadena
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    if id, ok := ctx.Value(claveIDSolicitud).(string); ok {
        log.Printf("Procesando solicitud %s", id)
    }
}

Detectar cancelación en goroutines

func worker(ctx context.Context, trabajo <-chan string) {
    for {
        select {
        case <-ctx.Done():
            log.Printf("Worker cancelado: %v", ctx.Err())
            return
        case tarea, ok := <-trabajo:
            if !ok {
                return // canal cerrado
            }
            procesarTarea(ctx, tarea)
        }
    }
}

Jerarquía de contextos

Los contextos forman una jerarquía: cancelar un padre cancela automáticamente todos los hijos:

func manejarPeticion(w http.ResponseWriter, r *http.Request) {
    // r.Context() es el contexto de la petición HTTP (padre)
    ctx := r.Context()

    // Crear un hijo con timeout más restrictivo para la BD
    ctxBD, cancelBD := context.WithTimeout(ctx, 2*time.Second)
    defer cancelBD()

    // Si r.Context() es cancelado (cliente desconectado),
    // ctxBD también queda cancelado automáticamente
    usuario, err := db.QueryRowContext(ctxBD, "SELECT * FROM usuarios WHERE id = $1",
        extractID(r)).Scan(...)
    ...
}
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Go es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Go

Explora más contenido relacionado con Go y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender la interfaz context.Context y sus cuatro métodos.
  • Crear contextos con cancelación usando context.WithCancel.
  • Establecer límites de tiempo con context.WithTimeout y context.WithDeadline.
  • Propagar valores transversales con context.WithValue de forma segura.
  • Encadenar contextos en una jerarquía padre-hijo.
  • Aplicar context correctamente en servidores HTTP, bases de datos y goroutines.