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
Contents
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. Gratisdefer cancel()siempre. Si no lo ponés, el timer queda vivo hasta que vence.go vette 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
Sign in to join the conversation.
Sign in to comment