Paquete context en Go
El paquete context en Go es fundamental para el manejo de cancelaciones, timeouts y valores a través de la jerarquía de llamadas. Este documento explora en detalle sus características y usos comunes.
1. Conceptos Básicos
1.1 Contextos Base
// Contextos base sin valores ni cancelación
func ejemplosContextosBase() {
// Background - para uso general
ctx := context.Background()
// TODO - para cuando aún no está claro qué contexto usar
ctxTodo := context.TODO()
}
1.2 Cancelación Manual
func ejemploCancelacion() {
// Crear contexto con cancelación
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Ejecutar tarea en goroutine
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Tarea cancelada")
return
default:
fmt.Println("Trabajando...")
time.Sleep(500 * time.Millisecond)
}
}
}()
// Simular trabajo por 2 segundos
time.Sleep(2 * time.Second)
cancel() // Cancelar la tarea
}
2. Timeouts y Deadlines
2.1 Timeout Simple
func ejemploTimeout() error {
// Crear contexto con timeout de 2 segundos
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return realizarOperacionLenta(ctx)
}
func realizarOperacionLenta(ctx context.Context) error {
doneChan := make(chan struct{})
go func() {
// Simular operación lenta
time.Sleep(3 * time.Second)
close(doneChan)
}()
select {
case <-ctx.Done():
return fmt.Errorf("operación cancelada por timeout: %w", ctx.Err())
case <-doneChan:
return nil
}
}
2.2 Deadline Específica
func ejemploDeadline() error {
// Crear contexto que expira a medianoche
midnight := time.Now().Add(24 * time.Hour)
midnight = time.Date(
midnight.Year(),
midnight.Month(),
midnight.Day(),
0, 0, 0, 0,
midnight.Location(),
)
ctx, cancel := context.WithDeadline(context.Background(), midnight)
defer cancel()
return realizarTareasProgramadas(ctx)
}
3. Valores en Contexto
3.1 Gestión de Valores
type ctxKey string
func ejemploValores() {
const (
userIDKey ctxKey = "userID"
sessionKey ctxKey = "session"
requestKey ctxKey = "requestID"
)
ctx := context.Background()
ctx = context.WithValue(ctx, userIDKey, "123")
ctx = context.WithValue(ctx, sessionKey, "abc-456")
ctx = context.WithValue(ctx, requestKey, "req-789")
procesarSolicitud(ctx)
}
func procesarSolicitud(ctx context.Context) {
// Extraer valores de forma segura
if userID, ok := ctx.Value(userIDKey).(string); ok {
fmt.Printf("Procesando solicitud para usuario: %s\n", userID)
}
}
4. Patrones de Uso Común
4.1 Cliente HTTP con Timeout
func clienteHTTPConTimeout() {
client := &http.Client{}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.ejemplo.com", nil)
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Println("La solicitud excedió el timeout")
return
}
log.Fatal(err)
}
defer resp.Body.Close()
}
4.2 Propagación de Cancelación
func trabajoConcurrente() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errChan := make(chan error, 3)
// Iniciar múltiples trabajadores
for i := 0; i < 3; i++ {
go func(id int) {
errChan <- trabajador(ctx, id)
}(i)
}
// Si cualquier trabajador falla, cancelar todos
for i := 0; i < 3; i++ {
if err := <-errChan; err != nil {
cancel() // Cancela otros trabajadores
return fmt.Errorf("error en trabajador: %w", err)
}
}
return nil
}
func trabajador(ctx context.Context, id int) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Realizar trabajo
time.Sleep(100 * time.Millisecond)
}
}
}
5. Mejores Prácticas
5.1 Middleware con Contexto
func middlewareConContexto(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Agregar valores al contexto
ctx := r.Context()
ctx = context.WithValue(ctx, "requestID", uuid.New().String())
ctx = context.WithValue(ctx, "startTime", time.Now())
// Establecer timeout para la solicitud
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Llamar al siguiente handler con el contexto modificado
next.ServeHTTP(w, r.WithContext(ctx))
})
}
6. Consideraciones y Buenas Prácticas
- Cancelación
- Siempre llamar a
cancel()cuando termines - Usar
defer cancel()cuando sea posible - Propagar la cancelación a todas las goroutines
- Siempre llamar a
- Valores
- Usar tipos personalizados para las claves
- No almacenar valores nil
- Mantener el contexto inmutable
- Timeouts
- Establecer timeouts apropiados
- Considerar el timeout total de la operación
- Manejar timeouts en cascada
- Errores
- Verificar
ctx.Err()para determinar la causa - Propagar errores de contexto apropiadamente
- No ignorar cancelaciones
- Verificar