Lección 11 de 20 · Lectura y práctica
Introducción a goroutines para procesamiento paralelo
Concepto claveLas 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.
Del concepto a la práctica
Taller de esta lección
Una goroutine es útil cuando el trabajo puede solaparse y existe una forma clara de esperar su finalización. Calcula el total de tres lotes independientes; el resultado correcto no depende del orden en que terminen, pero sí de esperar a todos.
Ejemplo resuelto
El programa completo de abajo incluye imports, workers, coordinación y consumidor.
Los valores producidos son 16, 49 y 81; su suma es 146.
Pruébalo
Lanza una goroutine por valor usando el parámetro local.
Espera con
WaitGroup.Cierra el channel después de esperar.
Suma todos los resultados recibidos.
Error frecuente: cerrar el channel desde una goroutine trabajadora sin coordinar a las demás. Corrígelo haciendo que el productor que conoce el final cierre después de Wait.
Comprueba tu resultado
La suma debe ser exactamente 146 en varias ejecuciones. El programa no debe terminar antes de recibir tres resultados ni provocar un cierre duplicado.
Programa completo y coordinación limitada
La suma16+49+81=146 no depende del orden. Este archivo main.go declara imports y consumidor; buffer3 es suficiente para los tres envíos antes de Wait, no una regla universal para cualquier volumen.
package main
import ("fmt";"sync")
func main(){
var wg sync.WaitGroup
results:=make(chan int,3)
for _,n:=range []int{4,7,9}{
wg.Add(1)
go func(v int){defer wg.Done();results<-v*v}(n)
}
wg.Wait();close(results)
total,count:=0,0
for v:=range results{total+=v;count++}
fmt.Println(count,total)
}Salida esperada3 146. Si reduces el buffer a0 y mantienes Wait antes de consumir, los envíos bloquean y Wait no termina; consume concurrentemente o mantén el contrato. WaitGroup.Add precede lanzamiento y Done se ejecuta por defer. Canales no vuelven seguros todos los mapas/punteros que otras goroutines siguen mutando.
net/http atiende solicitudes concurrentes sin necesitar go por cada handler. No paralelices validar, insertar y notificar si dependen de éxito anterior: valida, confirma escritura y entonces encola consecuencia. No uses ResponseWriter después del retorno del handler. Una goroutine en memoria con202 no es cola durable ni garantía de completar tras reiniciar.

Mapa de prácticaUna 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
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.
Pistas
- 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.
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.