context.Context en Go: cancelación y timeouts entre goroutines

context.Context es la forma estándar en Go de transportar señales de cancelación, deadlines y valores a través de capas de goroutines. Su regla más importante es tan simple como ignorada por los principiantes: el contexto siempre va como primer parámetro de toda función que pueda bloquearse o tardar.

El tipo Context

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

Done() devuelve un canal que se cierra cuando el contexto se cancela o expira. Err() explica el motivo: context.Canceled o context.DeadlineExceeded.

WithCancel: cancelación manual

package main

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

func tarea(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("tarea %d cancelada: %vn", id, ctx.Err())
            return
        default:
            fmt.Printf("tarea %d trabajandon", id)
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // buena práctica: siempre hacer defer cancel()

    go tarea(ctx, 1)
    go tarea(ctx, 2)

    time.Sleep(600 * time.Millisecond)
    cancel() // señala a todas las goroutines que paren
    time.Sleep(100 * time.Millisecond)
}

WithTimeout: límite de tiempo

func buscarConTimeout(query string) ([]string, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    resultadosCh := make(chan []string, 1)

    go func() {
        resultados := buscarEnBD(ctx, query) // pasa el contexto a la DB
        resultadosCh <- resultados
    }()

    select {
    case r := <-resultadosCh:
        return r, nil
    case <-ctx.Done():
        return nil, fmt.Errorf("búsqueda: %w", ctx.Err())
    }
}

WithDeadline: punto de tiempo absoluto

// Deadline absoluto: cancelar a las 14:00:00
deadline := time.Date(2026, 7, 1, 14, 0, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

La diferencia entre WithTimeout y WithDeadline es solo de conveniencia: WithTimeout(ctx, d) equivale a WithDeadline(ctx, time.Now().Add(d)).

Pasar el contexto a net/http

El cliente HTTP estándar acepta contextos a través de http.NewRequestWithContext:

func obtenerURL(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("GET %s: %w", url, err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

Pasar el contexto a database/sql

func obtenerUsuario(ctx context.Context, db *sql.DB, id int) (*Persona, error) {
    var p Persona
    err := db.QueryRowContext(ctx,
        "SELECT nombre, edad FROM personas WHERE id = $1", id,
    ).Scan(&p.Nombre, &p.Edad)
    if err != nil {
        return nil, fmt.Errorf("obtener usuario %d: %w", id, err)
    }
    return &p, nil
}

WithValue: transportar valores sin parámetros extra

type claveContexto string

const claveRequestID claveContexto = "request-id"

func handler(w http.ResponseWriter, r *http.Request) {
    id := uuid.New().String()
    ctx := context.WithValue(r.Context(), claveRequestID, id)
    r = r.WithContext(ctx)
    siguienteHandler(r)
}

func siguienteHandler(r *http.Request) {
    id, _ := r.Context().Value(claveRequestID).(string)
    fmt.Println("request-id:", id)
}

Usa tipos privados como clave para evitar colisiones con otros paquetes. No guardes valores que podrían pasarse como parámetros normales de función.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP