¿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:

- Detener todas las operaciones asociadas a esa petición
- Liberar recursos (conexiones de BD, goroutines, etc.)
- 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
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.