Skip to content
article

context.Context en Go, explicado como se lo explicaría a un amigo en un asado

timeouts, cancelaciones y por qué no tenés que meter cualquier cosa ahí adentro

Martín Ferraro@tincho_go

August 12, 2026 · 3 min read

Che, Camilo me preguntó el otro día "¿para qué sirve el ctx que todo el mundo pasa como primer parámetro?" y le contesté con un audio de 7 minutos. Lo paso a texto para que le sirva a alguien más.

Qué es, en una frase

context.Context es un valor que viaja con tu request y lleva tres cosas: un deadline (hasta cuándo tenés tiempo), una señal de cancelación (che, pará, ya no hace falta) y algunos valores del request (un request ID, un trace). Nada más. No es una bolsa para meter cosas.

Regla 1: primer parámetro, se llama ctx

func (s *Service) GetUser(ctx context.Context, id int64) (*User, error)

Siempre primero, siempre ctx. No lo guardes en un struct. Lo pasás de función en función, como el mate en la ronda.

Regla 2: timeout en todo lo que sale de tu proceso

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }

    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    var u User
    err = h.db.QueryRowContext(ctx,
        "SELECT id, name FROM users WHERE id = $1", id,
    ).Scan(&u.ID, &u.Name)
    if errors.Is(ctx.Err(), context.DeadlineExceeded) {
        http.Error(w, "timeout", http.StatusGatewayTimeout)
        return
    }
    if err != nil {
        http.Error(w, "error interno", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(u)
}

Dos cosas importantes ahí:

  • r.Context() se cancela solo cuando el cliente corta la conexión. Si el usuario cerró la pestaña, tu query también se cancela. Gratis
  • defer cancel() siempre. Si no lo ponés, el timer queda vivo hasta que vence. go vet te avisa (el chequeo se llama lostcancel), hacele caso

Regla 3: las goroutines escuchan ctx.Done()

El clásico leak: lanzás una goroutine que procesa jobs y nunca termina porque nadie le avisa que se terminó la fiesta.

func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            return // nos cancelaron, chau
        case j, ok := <-jobs:
            if !ok {
                return // cerraron el canal, chau
            }
            process(ctx, j)
        }
    }
}

Regla 4: valores en el context, los mínimos

Request ID, trace, y hay quien mete el usuario autenticado (hay discusión, yo prefiero pasarlo explícito). Nunca dependencias como la conexión a la base, ni parámetros opcionales. Si una función necesita algo, que lo pida como parámetro. Y la key siempre de un tipo propio, no exportado, para que nadie te la pise:

type requestIDKey struct{}

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey{}, id)
}

func RequestID(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey{}).(string)
    return id
}

Bonus: trabajo después de responder

Si querés mandar un mail después de responder el request, no uses r.Context() porque se cancela apenas respondés. Desde Go 1.21 está context.WithoutCancel(ctx): conserva los valores (el request ID para los logs) pero no se cancela con el padre. Ponele su propio timeout igual, no seas bestia.

Para leer más, la doc oficial está muy bien: pkg.go.dev/context, y el post viejo pero vigente del blog de Go: go.dev/blog/context.

Camilo, ahí tenés. Me debés un fernet.

Written by

Martín Ferraro

Backend en Go, Buenos Aires. Laburo para afuera, cobro en dólares, pago en pesos. Che, ¿alguien sabe a cuánto está el blue hoy?

Get the best of #codigo_en_espanol weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

7

Sign in to join the conversation.

Sign in to comment
Camilo Restrepo@camilo_parce#3 in Código en Español
Uff parce justo esto me tenía enredado. Lo de que r.Context() se cancela cuando el cliente se va no lo sabía, eso explica unos logs raros que tengo. Gracias pues! El fernet va, aunque no sé qué es
0 net votes
Martín Ferraro@tincho_go#2 in Código en Español
seguro tenías queries que seguían corriendo después de que el usuario cerró la pestaña. clásico. y el fernet ya lo vas a conocer, no hay vuelta atrás
0 net votes
Sofía Ramírez@sofi_dev_mx#1 in Código en Español
no escribo Go pero lo de 'no metas cualquier cosa en el context' aplica igualito a React Context jaja, la gente mete todo ahí
1 net votes
Emeka Nwosu@emeka_golang1 rep in Código en Español
Translated this, solid post my guy. One add: in tests use t.Context() if you're on Go 1.24+, it's cancelled when the test ends. Saves a lot of boilerplate 🤣
1 net votes
Martín Ferraro@tincho_go#2 in Código en Español
Good one, thanks! one service at work is still on an old version, the upgrade is in the backlog since forever jaja
0 net votes
Luis Treviño@luis_qa_mty#8 in Código en Español
yo soy de QA pero guardado para cuando los devs me digan 'es que el timeout es del lado de QA' 🤣
0 net votes
Javier Montero@javi_montero#4 in Código en Español
Muy buen post. En el banco seguimos con Java 8 en la mitad de los servicios y esto me parece ciencia ficción. Vaya tela.
2 net votes