Concepto clave
Las goroutines son la unidad fundamental de concurrencia en Go. Piensa en ellas como hilos ligeros que el runtime de Go gestiona de manera eficiente sobre un número limitado de hilos del sistema operativo. A diferencia de los hilos tradicionales, que pueden consumir megabytes de memoria y tienen un alto costo de creación, las goroutines comienzan con solo 2KB de stack y pueden crecer dinámicamente.
Una analogía del mundo real: imagina un restaurante donde un solo camarero (hilo del sistema) atiende múltiples mesas (goroutines). En lugar de dedicarse completamente a una mesa hasta que termine, el camarero toma un pedido en una mesa, luego pasa a otra mientras la cocina prepara la comida, optimizando así el tiempo. El scheduler de Go actúa como el gerente que asigna las tareas del camarero.
La clave para APIs de alto rendimiento es que las goroutines permiten manejar miles de conexiones concurrentes sin bloquear el procesamiento. Cuando una goroutine espera una operación de I/O (como una consulta a base de datos), el scheduler automáticamente la pausa y ejecuta otra goroutine lista, maximizando el uso de CPU.
Cómo funciona en la práctica
Para crear una goroutine, simplemente antepones la palabra clave go a una llamada de función. Esto lanza la ejecución de esa función en una nueva goroutine, mientras el programa principal continúa su flujo. Veamos un ejemplo paso a paso:
- Defines una función que realiza una tarea, como procesar una solicitud HTTP.
- Usas
go funcion()para ejecutarla concurrentemente. - El runtime de Go gestiona la ejecución, alternando entre goroutines según disponibilidad.
En el contexto de microservicios REST, esto significa que puedes manejar múltiples solicitudes de API simultáneamente. Por ejemplo, al recibir una petición POST para crear un usuario, puedes lanzar una goroutine para validar los datos, otra para insertar en la base de datos, y otra para enviar un correo de confirmación, todo sin bloquear la respuesta al cliente.
Código en acción
Aquí tienes un ejemplo básico que muestra la diferencia entre ejecución secuencial y concurrente:
package main
import (
"fmt"
"time"
)
func tarea(nombre string, duracion time.Duration) {
fmt.Printf("Iniciando %s\n", nombre)
time.Sleep(duracion)
fmt.Printf("Finalizando %s\n", nombre)
}
func main() {
// Versión secuencial (lenta)
fmt.Println("--- Ejecución secuencial ---")
tarea("Tarea A", 2*time.Second)
tarea("Tarea B", 1*time.Second)
// Versión concurrente con goroutines (rápida)
fmt.Println("\n--- Ejecución concurrente ---")
go tarea("Tarea C", 2*time.Second)
go tarea("Tarea D", 1*time.Second)
// Espera para que las goroutines terminen (en producción usarías channels o WaitGroup)
time.Sleep(3 * time.Second)
}Ahora, un ejemplo más realista para una API REST con gorilla/mux, donde manejamos solicitudes HTTP concurrentemente:
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
func procesarUsuario(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
// Simula procesamiento pesado (ej., validación, base de datos)
go func() {
time.Sleep(100 * time.Millisecond) // Simula I/O
fmt.Printf("Usuario %s procesado en segundo plano\n", user.Name)
}()
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"status": "procesando"})
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/users", procesarUsuario).Methods("POST")
fmt.Println("Servidor escuchando en puerto 8080")
http.ListenAndServe(":8080", r)
}Errores comunes
- No sincronizar goroutines: Lanzar goroutines sin mecanismos de sincronización como
sync.WaitGroupo channels puede hacer que el programa termine antes de que completen, perdiendo datos. Solución: UsaWaitGrouppara esperar que todas las goroutines finalicen. - Compartir memoria sin protección: Múltiples goroutines accediendo a la misma variable pueden causar condiciones de carrera. Solución: Usa mutex (
sync.Mutex) o channels para comunicación segura. - Crear goroutines en bucles sin control: Un bucle que crea goroutines ilimitadas puede agotar la memoria. Solución: Usa un worker pool o limita la concurrencia con un semáforo.
- Ignorar el manejo de errores en goroutines: Los errores en goroutines no se propagan automáticamente al goroutine principal. Solución: Usa channels para enviar errores o implementa un patrón de supervisión.
- Asumir orden de ejecución: Las goroutines se ejecutan de manera no determinística; no dependas de un orden específico. Solución: Diseña tu código para ser independiente del orden, usando sincronización cuando sea necesario.
Checklist de dominio
- Puedo explicar la diferencia entre concurrencia y paralelismo en mis propias palabras.
- Sé crear una goroutine usando la palabra clave
goen una función o función anónima. - He usado
sync.WaitGrouppara esperar que múltiples goroutines terminen. - Puedo identificar y evitar condiciones de carrera usando mutex o channels.
- He implementado un patrón básico de worker pool para limitar la concurrencia.
- Sé manejar errores en goroutines enviándolos a un channel.
- Puedo integrar goroutines en un handler HTTP de gorilla/mux para procesamiento en segundo plano.
Refactorizar un handler secuencial a concurrente
En este ejercicio, refactorizarás un endpoint de API REST que actualmente procesa solicitudes de manera secuencial para usar goroutines y mejorar el rendimiento.
- Contexto: Tienes un microservicio Go con gorilla/mux que maneja solicitudes POST a
/batch. Cada solicitud contiene un array de hasta 100 items que deben procesarse individualmente (simulado con un sleep). Actualmente, el procesamiento es secuencial, lo que hace lento el endpoint. - Paso 1: Clona o crea un archivo Go con el siguiente código base:
package main import ( "encoding/json" "net/http" "time" "github.com/gorilla/mux" ) type BatchRequest struct { Items []string `json:"items"` } func procesarItem(item string) { // Simula procesamiento (ej., llamada a API externa o DB) time.Sleep(50 * time.Millisecond) } func batchHandler(w http.ResponseWriter, r *http.Request) { var req BatchRequest json.NewDecoder(r.Body).Decode(&req) // Versión secuencial (a refactorizar) for _, item := range req.Items { procesarItem(item) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "completado"}) } func main() { r := mux.NewRouter() r.HandleFunc("/batch", batchHandler).Methods("POST") http.ListenAndServe(":8080", r) } - Paso 2: Refactoriza
batchHandlerpara usar goroutines. Procesa todos los items concurrentemente. Asegúrate de sincronizar las goroutines usandosync.WaitGroup. - Paso 3: Agrega manejo básico de errores. Si alguna goroutine falla (simula un error aleatorio), registra el error pero permite que las demás continúen.
- Paso 4: Prueba tu endpoint con una herramienta como curl o Postman. Envía una solicitud con 10 items y verifica que el tiempo de respuesta mejora respecto a la versión secuencial.
- Paso 5: Opcional, implementa un límite de concurrencia usando un worker pool de 5 goroutines para evitar sobrecargar el sistema.
- Usa sync.WaitGroup para esperar a que todas las goroutines de procesamiento terminen antes de enviar la respuesta HTTP.
- Dentro del bucle, incrementa el WaitGroup antes de lanzar cada goroutine y llama a Done() al final de la goroutine.
- Para manejar errores, considera usar un channel de errores o una variable compartida protegida por un mutex.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.