Saltar al contenido

Lección 14 de 20 · Lectura y práctica

Práctica: Optimizar una API con goroutines para tareas pesadas

Concepto claveEn el desarrollo de APIs de alto rendimiento con Go, la concurrencia es tu aliado principal para manejar tareas pesadas sin bloquear las respuestas HTTP. Imagina un restaurante donde un solo camarero atiende todas.

Del concepto a la práctica

Taller de esta lección

Optimizar no significa lanzar goroutines indiscriminadamente. Compara un cálculo local de cuatro lotes pequeños de 20 enteros como primera comprobación: primero ejecútalos en serie y después con un número fijo de workers. La métrica útil es el tiempo medido en el mismo equipo, no una cifra prometida.

Ejemplo resuelto

Divide el trabajo en rangos [0,20), [20,40), [40,60) y [60,80) enteros. Cada worker devuelve una suma parcial y el handler combina las cuatro. El resultado matemático debe coincidir entre ambas versiones.

Pruébalo

  1. Implementa una función pura que calcule una suma para un rango.

  2. Mide la versión secuencial con time.Since.

  3. Ejecuta cuatro tareas mediante un pool de dos workers.

  4. Compara resultado y duración, sin asumir mejora.

Error frecuente: medir solo la variante concurrente o compartir un acumulador sin protección. Corrígelo midiendo ambas y agregando resultados por channel.

Comprueba tu resultado

Las dos versiones deben producir la misma suma. El informe debe incluir ambos tiempos medidos, el número de workers y una explicación si la variante concurrente no es más rápida.

Suma pequeña antes de medir carga

La primera comprobación usa rangos enteros semiabiertos [0,20),[20,40),[40,60),[60,80): contienen20 valores cada uno y suman0..79=3160. La versión secuencial y el pool de dos workers deben producir3160, sin compartir un acumulador mutable. Usa int64 para variantes de80millones: sumar0..79,999,999 produce3,199,999,960,000,000, superior aint32.

go
func sumRange(start,end int64) int64{
  var total int64
  for n:=start;n<end;n++{total+=n}
  return total
}

Primero comprueba sumRange(0,20)=190 y sumRange(60,80)=1390. Luego conecta esas cuatro tareas al patrón13 y suma resultados en consumidor. Medir80 iteraciones puede estar dominado por overhead; no infieras mejora de CPU ni publiques un tiempo inventado. Una medición mayor requiere mismo hardware, entrada, calentamiento y límites adecuados; no es obligatoria para comprobar esta fórmula.

Esperar una tarea en un handler retrasa esa respuesta, no demuestra que todas las demás peticiones HTTP queden bloqueadas. Responder202 reduce tiempo de aceptación, no duración del trabajo. El trabajo ligado al request observa r.Context(); uno durable aceptado tiene ciclo de vida del servicio y su propia política, porque r.Context se cancela al volver el handler.

Diagrama de decisión con ramas Sí y No y un bucle que procesa cada elemento de una lista.
CursaloMapa de práctica
Cómo avanza un programa

Una condición decide qué camino ejecuta el programa. Un bucle repite una operación mientras queden elementos, y termina cuando la lista se agota. Separar ambas ideas ayuda a seguir el flujo y comprobar casos límite.

Ver la infografía en tamaño completo ↗

Concepto clave

En el desarrollo de APIs de alto rendimiento con Go, la concurrencia es tu aliado principal para manejar tareas pesadas sin bloquear las respuestas HTTP. Imagina un restaurante donde un solo camarero atiende todas las mesas secuencialmente: si una mesa pide un plato que tarda 30 minutos en prepararse, todas las demás mesas esperan. Con goroutines, es como tener múltiples camareros que pueden tomar pedidos simultáneamente, delegando las tareas lentas a la cocina (background) mientras siguen atendiendo a otros clientes.

Las goroutines son hilos ligeros gestionados por el runtime de Go, que permiten ejecutar funciones concurrentemente con un overhead mínimo. Cuando optimizas una API, el objetivo es identificar operaciones bloqueantes (como procesamiento de datos, llamadas a bases de datos lentas, o cálculos intensivos) y moverlas a goroutines para que el hilo principal pueda seguir respondiendo a nuevas peticiones. Esto mejora la latencia (tiempo de respuesta por petición) y el throughput (número de peticiones manejadas por segundo), clave para microservicios REST.

Cómo funciona en la práctica

Para optimizar una API con goroutines, sigue estos pasos en un proyecto real con gorilla/mux:

  1. Identifica el cuello de botella: Usa profiling (como pprof) o métricas para encontrar endpoints con operaciones lentas que no dependen secuencialmente de otras.
  2. Aísla la tarea pesada: Refactoriza el código para extraer la lógica lenta en una función independiente que pueda ejecutarse concurrentemente.
  3. Lanza la goroutine: Usa la palabra clave go para ejecutar la función en segundo plano, manejando la sincronización con channels o waitgroups si necesitas resultados.
  4. Respuesta inmediata: Devuelve una respuesta HTTP al cliente (ej., un ID de proceso o estado "en progreso") mientras la goroutine completa el trabajo en background.
  5. Maneja errores y recursos: Asegúrate de que las goroutines no generen leaks y gestionen panics para mantener la estabilidad del servidor.

Ejemplo: En una API de procesamiento de imágenes, un endpoint que sube una imagen, la redimensiona, aplica filtros y guarda en disco podría bloquearse por segundos. Al mover el procesamiento a una goroutine, el endpoint responde inmediatamente con "Imagen recibida, procesando...", mejorando la experiencia del usuario.

Codigo en accion

Aquí un ejemplo antes y después de optimizar un endpoint con goroutines. Supongamos un endpoint POST /process que simula una tarea pesada (ej., generar un reporte).

Antes: API bloqueante

package main

import (
    "fmt"
    "net/http"
    "time"
    "github.com/gorilla/mux"
)

func heavyTask() {
    // Simula una tarea que tarda 3 segundos
    time.Sleep(3 * time.Second)
    fmt.Println("Tarea pesada completada")
}

func processHandler(w http.ResponseWriter, r *http.Request) {
    heavyTask() // Bloquea la respuesta hasta que termine
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Procesamiento completado"))
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/process", processHandler).Methods("POST")
    http.ListenAndServe(":8080", r)
}

Después: API optimizada con goroutine

package main

import (
    "fmt"
    "net/http"
    "time"
    "github.com/gorilla/mux"
)

func heavyTask() {
    time.Sleep(3 * time.Second)
    fmt.Println("Tarea pesada completada")
}

func processHandler(w http.ResponseWriter, r *http.Request) {
    go heavyTask() // Ejecuta en background sin bloquear
    w.WriteHeader(http.StatusAccepted)
    w.Write([]byte("Procesamiento iniciado en background"))
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/process", processHandler).Methods("POST")
    http.ListenAndServe(":8080", r)
}

En la versión optimizada, el handler responde inmediatamente con estado 202 (Accepted), mientras la tarea se ejecuta concurrentemente. Esto permite al servidor manejar múltiples peticiones simultáneas sin esperar.

Errores comunes

  • No manejar panics en goroutines: Si una goroutine entra en panic, puede crashear todo el programa. Usa defer con recover() para capturar y loguear errores.
  • Ignorar la sincronización: Al lanzar goroutines sin channels o waitgroups, puedes perder resultados o causar race conditions. Para tareas que necesitan coordinación, usa sync.WaitGroup o channels.
  • Crear goroutines en bucle sin límite: Lanzar miles de goroutines para cada petición puede agotar recursos. Implementa un worker pool o limita la concurrencia con buffered channels.
  • No considerar el contexto HTTP: Si una petición se cancela (ej., cliente cierra la conexión), las goroutines asociadas deberían detenerse. Pasa el context.Context del request para manejar cancelaciones.
  • Olvidar el garbage collection: Goroutines huérfanas pueden causar memory leaks. Asegúrate de que todas las goroutines terminen adecuadamente, especialmente en casos de error.

Checklist de dominio

  1. Identifico al menos una operación bloqueante en mis endpoints que pueda moverse a una goroutine.
  2. Uso go para ejecutar tareas pesadas en background, devolviendo respuestas HTTP inmediatas.
  3. Implemento manejo de errores con recover() en goroutines para prevenir crashes.
  4. Sincronizo goroutines con sync.WaitGroup o channels cuando necesito resultados o coordinación.
  5. Limito la concurrencia con patrones como worker pools para evitar sobrecarga del sistema.
  6. Utilizo context.Context para propagar cancelaciones y timeouts desde las peticiones HTTP.
  7. Mido la mejora en latencia y throughput usando herramientas como pprof o métricas personalizadas.

Optimiza un endpoint de reportes con goroutines y channels

En este ejercicio, refactorizarás un endpoint de API que genera reportes pesados para usar goroutines y channels, mejorando su rendimiento.

  1. Configura el proyecto: Crea un nuevo directorio para el ejercicio e inicializa un módulo Go con go mod init ejercicio-reportes. Instala gorilla/mux con go get github.com/gorilla/mux.
  2. Escribe el código inicial: Crea un archivo main.go con un endpoint POST /generate-report que simule generar un reporte tardando 5 segundos (usa time.Sleep). El handler debe bloquearse hasta completar y responder con "Reporte generado".
  3. Identifica la mejora: Ejecuta el servidor y prueba con curl -X POST http://localhost:8080/generate-report. Nota cómo la respuesta tarda 5 segundos, bloqueando otras peticiones.
  4. Implementa goroutines con channels: Refactoriza el handler para lanzar una goroutine que genere el reporte. Usa un channel de tipo chan string para recibir el resultado (ej., el contenido del reporte). El handler debe responder inmediatamente con "Generando reporte, ID: X" y luego, en una goroutine separada, esperar el resultado del channel y loguearlo.
  5. Añade manejo de errores: Modifica la goroutine para incluir un defer con recover() que capture cualquier panic y lo loguee, enviando un mensaje de error al channel si ocurre.
  6. Prueba la optimización: Ejecuta el servidor nuevamente y haz múltiples peticiones simultáneas con curl o una herramienta como Apache Bench. Verifica que las respuestas sean inmediatas y que el reporte se genere en background.
Pistas
  • Usa go func() { ... }() para lanzar la goroutine y un channel para comunicar el resultado.
  • Considera usar un buffered channel si necesitas manejar múltiples reportes simultáneamente sin bloquear.
  • No olvides cerrar el channel después de usarlo para evitar leaks.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.

Sigue con Go con Gorilla/Mux: CRUD, Validación y Microservicios

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.