Uno de los mayores atractivos de Go (Golang) es su modelo de concurrencia de primera clase, basado en los procesos secuenciales comunicados (CSP - Communicating Sequential Processes). A diferencia de otros lenguajes que dependen de hilos pesados del sistema operativo o complejas cadenas de callbacks, Go facilita la ejecución concurrente ligera y segura.
En este artículo exploraremos desde los fundamentos básicos hasta patrones de diseño avanzados para construir aplicaciones concurrentes y escalables.
¿Qué es una Goroutine?
Una goroutine es una función que se ejecuta de forma concurrente con otras goroutines en el mismo espacio de direcciones. Son extraordinariamente ligeras: mientras que un hilo del sistema operativo suele requerir 1-2 MB de pila, una goroutine comienza con apenas 2 KB. Esto permite ejecutar decenas de miles de goroutines simultáneamente sin agotar la memoria.
Para iniciar una goroutine solo necesitas anteponer la palabra clave go:
package main
import (
"fmt"
"time"
)
func decir(texto string) {
for i := 0; i < 3; i++ {
fmt.Println(texto)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go decir("Hola") // Se ejecuta en segundo plano
decir("Mundo") // Se ejecuta en el hilo principal
}
Comunicación segura mediante Channels
El mantra oficial del equipo de Go dice:
“No te comuniques compartiendo memoria; comparte memoria comunicándote.”
Los channels (canales) son los conductos que permiten enviar y recibir valores entre distintas goroutines, sincronizando la ejecución de forma automática.
Ejemplo de Canal Unbuffered (Sin búfer)
Un canal sin búfer bloquea la ejecución del emisor hasta que un receptor esté listo para leer la información:
package main
import "fmt"
func calcularCuadrado(numero int, ch chan int) {
resultado := numero * numero
ch <- resultado // Envía el resultado al canal
}
func main() {
ch := make(chan int)
go calcularCuadrado(5, ch)
valor := <-ch // Recibe el valor del canal (bloqueante)
fmt.Printf("El resultado es: %d\n", valor)
}
Patrón Avanzado: Worker Pool
El patrón Worker Pool permite procesar un volumen elevado de tareas limitando el número máximo de goroutines concurrentes, evitando así la saturación de CPU o recursos externos (como bases de datos o APIs).
package main
import (
"fmt"
"time"
)
func worker(id int, tareas <-chan int, resultados chan<- int) {
for t := range tareas {
fmt.Printf("Worker %d procesando tarea %d\n", id, t)
time.Sleep(500 * time.Millisecond)
resultados <- t * 2
}
}
func main() {
const numTareas = 10
const numWorkers = 3
tareas := make(chan int, numTareas)
resultados := make(chan int, numTareas)
// Crear los trabajadores
for w := 1; w <= numWorkers; w++ {
go worker(w, tareas, resultados)
}
// Enviar tareas
for t := 1; t <= numTareas; t++ {
tareas <- t
}
close(tareas) // Cerramos el canal para avisar a los workers
// Recoger resultados
for r := 1; r <= numTareas; r++ {
fmt.Printf("Resultado recibido: %d\n", <-resultados)
}
}
Cancelación con context.Context y select
Para evitar fugas de goroutines (goroutine leaks), es indispensable poder cancelar tareas cuando se cumple un tiempo límite (timeout) o ante una señal de apagado.
package main
import (
"context"
"fmt"
"time"
)
func tareaLarga(ctx context.Context) {
select {
case <-time.After(2 * time.Second):
fmt.Println("Tarea completada con éxito")
case <-ctx.Done():
fmt.Println("Tarea cancelada:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go tareaLarga(ctx)
time.Sleep(1500 * time.Millisecond)
}
Conclusión
La concurrencia en Go no es solo una característica técnica; es una filosofía de diseño. Al dominar las goroutines, channels, estructuras como select y el paquete context, estarás preparado para construir sistemas backend modernos, altamente concurrentes y robustos.