El paquete context de Go resuelve un problema real: cómo cancelar una operación en curso que involucra varias goroutines y llamadas a servicios externos. En lugar de pasar múltiples canales de cancelación, un context.Context propaga la cancelación, los timeouts y los valores a lo largo de toda la cadena de llamadas con una única interfaz.
La interfaz Context
Context tiene cuatro métodos. Done() devuelve un canal que se cierra cuando se cancela el contexto. Err() explica por qué. Deadline() indica cuándo expira. Value(key) recupera valores:
type Context interface {
Done() <-chan struct{}
Err() error // context.Canceled o context.DeadlineExceeded
Deadline() (deadline time.Time, ok bool)
Value(key any) any
}
context.WithCancel
WithCancel devuelve un contexto derivado y una función cancel que al llamarla cierra el canal Done(). El defer cancel() es imprescindible para liberar recursos aunque la operación termine antes:
package main
import (
"context"
"fmt"
"time"
)
func operacionLarga(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil // terminó bien
case <-ctx.Done():
return ctx.Err() // cancelado o timeout
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Second)
cancel() // cancela desde otra goroutine
}()
err := operacionLarga(ctx)
fmt.Println(err) // context canceled
}
context.WithTimeout y WithDeadline
WithTimeout cancela el contexto automáticamente pasado el tiempo indicado. WithDeadline acepta un momento absoluto:
func buscarConDB(query string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// La query se cancela si tarda más de 3 segundos
rows, err := db.QueryContext(ctx, "SELECT nombre FROM usuarios WHERE rol = $1", query)
if err != nil {
return nil, fmt.Errorf("consulta fallida: %w", err)
}
defer rows.Close()
var resultados []string
for rows.Next() {
var nombre string
rows.Scan(&nombre)
resultados = append(resultados, nombre)
}
return resultados, rows.Err()
}
context.WithValue: pasar datos entre goroutines
WithValue adjunta un valor al contexto que se propaga automáticamente a todos los contextos derivados. Usa tipos propios como clave para evitar colisiones:
type contextKey string
const claveRequestID contextKey = "request_id"
func middlewareID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := generarID()
ctx := context.WithValue(r.Context(), claveRequestID, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
id, ok := r.Context().Value(claveRequestID).(string)
if !ok {
http.Error(w, "sin request ID", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "petición %s procesada", id)
}
Propagación en servidores HTTP
El contexto de una petición HTTP llega cancelado cuando el cliente desconecta. Propágalo a todas las llamadas descendientes para no seguir trabajando en vano:
func apiHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // lleva cancelación automática del cliente
// Añade timeout adicional para la llamada externa
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
resultado, err := llamadaExterna(ctx)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "timeout en servicio externo", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, resultado)
}
func llamadaExterna(ctx context.Context) (string, error) {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/data", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// leer respuesta...
return "ok", nil
}
Reglas de uso del contexto
- El primer argumento de cualquier función que haga I/O o llamadas de red deberÃa ser
ctx context.Context. - Nunca almacenes un contexto en una estructura; pásalo explÃcitamente.
- Usa
context.Background()en el arranque de la aplicación y en tests. - Usa
context.TODO()como marcador temporal cuando aún no sabes qué contexto usar. - Llama siempre al
canceldevuelto, aunque sea condefer, para no perder recursos.
