Go Goroutines y Channels: Guía Práctica de Concurrencia
Probablemente hayas escrito código Go que funciona bien hasta que deja de hacerlo—quizás una data race bloquea tu servicio bajo carga, o un deadlock congela tu API. La concurrencia es uno de los mayores atractivos de Go, pero también es donde muchos desarrolladores se quedan atascados. Las goroutines y los channels son los bloques de construcción, pero usarlos correctamente requiere entender su comportamiento y los errores comunes.
Esta guía recorre patrones prácticos para usar goroutines y channels, con ejemplos de código y consejos que puedes aplicar de inmediato.
¿Qué son las Goroutines?
Una goroutine es un hilo ligero gestionado por el runtime de Go. Se crea anteponiendo go a una llamada de función:
go doSomething()
Las goroutines comienzan con una pila pequeña (unos pocos kilobytes) que crece según sea necesario. Esto hace que sea barato crear miles o incluso millones de ellas. El planificador de Go multiplexa las goroutines sobre hilos del sistema operativo, así que no gestionas hilos directamente.
Pero crear una goroutine sin coordinación es una receta para el caos. Necesitas una forma de esperarlas, pasar datos o cancelarlas—ahí es donde entran los channels.
Channels: Comunicación Entre Goroutines
Los channels son conductos tipados para enviar y recibir valores. Proporcionan sincronización: un envío se bloquea hasta que un receptor está listo, y una recepción se bloquea hasta que hay un valor disponible. Esto hace que los channels sean ideales para coordinar goroutines.
Crea un channel con make(chan T):
ch := make(chan int)
go func() {
ch <- 42 // enviar
}()
value := <-ch // recibir
Por defecto, los channels no tienen búfer, lo que significa que los envíos y las recepciones ocurren al mismo ritmo. Los channels con búfer permiten un número limitado de envíos sin un receptor:
ch := make(chan int, 3) // tamaño de búfer 3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 se bloquearía hasta que alguien reciba
Los channels con búfer pueden suavizar ráfagas, pero no eliminan la necesidad de coordinación.
Patrones de Concurrencia Comunes
1. Pool de Workers
Distribuye tareas entre un número fijo de workers para limitar el uso de recursos:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
for r := 1; r <= 5; r++ {
<-results
}
}
Este patrón es excelente para tareas intensivas en CPU o IO donde quieres controlar el paralelismo.
2. Fan-Out, Fan-In
Fan-out significa que múltiples goroutines leen del mismo channel. Fan-in significa fusionar resultados de múltiples channels en uno:
func fanIn(chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chs))
for _, ch := range chs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Usa esto cuando tengas múltiples productores y quieras un único flujo de resultados.
3. Select para Multiplexación
La sentencia select permite que una goroutine espere en múltiples operaciones de channel:
select {
case msg := <-ch1:
fmt.Println("recibido de ch1:", msg)
case ch2 <- 42:
fmt.Println("enviado a ch2")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Usa select con un caso default para operaciones no bloqueantes, o con un timeout para evitar esperar para siempre.
Errores Comunes y Cómo Evitarlos
- Fugas de goroutines: Una goroutine bloqueada en un envío/recepción de channel para siempre nunca será recolectada por el garbage collector. Asegúrate siempre de que los channels se cierren o usa cancelación con context.
- Deadlocks: Las dependencias circulares entre channels causan deadlocks. Usa timeouts o diseña sin ciclos.
- Data races: Acceder a memoria compartida sin sincronización provoca races. Usa channels o mutexes para proteger el estado compartido.
- Cerrar channels: Solo el emisor debe cerrar un channel. Cerrar desde el receptor puede causar panics. Además, enviar en un channel cerrado provoca panic.
Mejores Prácticas
- Prefiere channels para coordinación, mutexes para estado: Usa channels para pasar datos y señales entre goroutines; usa
sync.Mutexpara proteger estado compartido simple. - Usa context para cancelación: Propaga
context.Contextpara permitir un apagado ordenado y timeouts. - Limita la concurrencia: Usa pools de workers o semáforos para evitar saturar los recursos.
- Prueba con el detector de races: Ejecuta
go test -racepara detectar data races a tiempo. - Manténlo simple: No sobreingenierices. A menudo un mutex es más simple y rápido que una coreografía compleja de channels.
Comparación: Channels vs. Mutexes
| Aspecto | Channels | Mutexes |
|---|---|---|
| Caso de uso | Pasar datos, coordinación | Proteger estado compartido |
| Complejidad | Mayor para tareas simples | Menor para tareas simples |
| Rendimiento | Sobrecarga por sincronización | Normalmente más rápido para secciones críticas cortas |
| Riesgo | Deadlocks, fugas | Deadlocks si no se tiene cuidado |
Preguntas Frecuentes
¿Cuándo debo usar un channel con búfer?
Usa un channel con búfer cuando quieras desacoplar emisores y receptores para manejar ráfagas, o cuando conozcas el número exacto de envíos y quieras evitar bloqueos. Sin embargo, los channels sin búfer ofrecen garantías de sincronización más fuertes y a menudo son más fáciles de razonar.
¿Cómo evito fugas de goroutines?
Asegúrate de que cada goroutine tenga una ruta de salida clara. Usa context.Context para señalar la cancelación, cierra los channels cuando no se enviarán más valores y evita bloquear indefinidamente en operaciones de channel sin un timeout o caso default.
¿Puedo usar channels para todo?
No. Aunque los channels son potentes, añaden sobrecarga y complejidad. Para estado compartido simple, un mutex suele ser más directo y eficiente. El proverbio de Go dice: "No te comuniques compartiendo memoria; comparte memoria comunicándote", pero eso no significa que los channels sean siempre la respuesta.
Si estás depurando problemas de concurrencia o analizando logs de tus servicios Go, el Nginx Log Analyzer puede ayudarte a detectar patrones y errores rápidamente.