Un channel unbuffered (sin búfer) es un mecanismo de comunicación sincrónica puro. Funciona mediante un sistema de rendezvous (punto de encuentro): para que una transferencia de datos ocurra, tanto el emisor (sender) como el receptor (receiver) deben estar listos en el mismo instante. Si intentas enviar un valor en un canal sin búfer y no hay nadie escuchando, la goroutine emisor se bloquea. Si intentas recibir de uno que no tiene datos y ningún emisor está listo, el receptor se bloquea. Esta transferencia garantiza que el emisor sabe, con certeza absoluta, que el receptor ha tomado el dato.
Un channel buffered (con búfer) introduce una capacidad de almacenamiento intermedia. El emisor puede depositar hasta $N$ valores (donde $N$ es el tamaño del búfer definido en make(chan T, N)) sin necesidad de que haya un receptor listo. Esto permite desacoplar el tiempo de ejecución del emisor del tiempo del receptor. El emisor solo se bloquea en dos escenarios: cuando el búfer está lleno (y quiere enviar más) o cuando el canal está cerrado. El receptor solo se bloquea cuando el búfer está vacío.
Debes usar canales sin búfer cuando necesites sincronización estricta o cuando el envío de un mensaje deba garantizar que otra parte del sistema ha procesado la señal. Es ideal para señales de cancelación o finalización. Por el contrario, usa canales con búfer para amortiguar ráfagas de trabajo (bursts) de un productor rápido, o para implementar semáforos limitando la concurrencia mediante make(chan struct{}, N).
Si usas un canal sin búfer en la misma goroutine donde intentas enviar y recibir, causarás un deadlock inmediato. Si decides usar un canal con búfer para “arreglar” un problema de rendimiento, ten cuidado: un búfer demasiado grande solo pospone el bloqueo cuando el canal se llene, ocultando un problema de diseño donde el consumidor es estructuralmente más lento que el productor.
package main
import (
"fmt"
"time"
)
func main() {
// jobs es un canal con búfer de tamaño 2. Permite desacoplar al productor.
jobs := make(chan int, 2)
// done es un canal sin búfer (unbuffered). Se usa para sincronizar el cierre.
done := make(chan bool)
// Goroutine Productor
go func() {
for i := 1; i <= 5; i++ {
fmt.Printf("[Productor] Intentando enviar trabajo %d...\n", i)
// El envío 1 y 2 entrarán al búfer inmediatamente.
// El envío 3 se bloqueará hasta que el consumidor libere espacio.
jobs <- i
fmt.Printf("[Productor] -> Trabajo %d enviado con éxito\n", i)
}
close(jobs)
}()
// Goroutine Consumidor
go func() {
for j := range jobs {
fmt.Printf("[Consumidor] Procesando trabajo %d...\n", j)
// Simulamos un proceso que tarda tiempo.
time.Sleep(1 * time.Second)
}
// Sincronizamos el fin del proceso con el main mediante un rendezvous.
done <- true
}()
// Esperamos la señal de sincronización para evitar que el programa termine antes.
<-done
fmt.Println("Proceso completo.")
}
Desglose del ejemplo
En el código anterior, la variable jobs es un canal con búfer de tamaño 2. Fíjate en la ejecución: cuando el productor ejecuta jobs <- 1 y jobs <- 2, estos valores se mueven directamente al búfer interno del canal y el productor continúa sin esperar. Sin embargo, cuando intenta ejecutar jobs <- 3, el búfer ya está lleno. En ese momento, el runtime de Go pone a la goroutine del productor en estado de espera (waiting).
La ejecución solo se desbloquea cuando el consumidor ejecuta for j := range jobs. Al leer el primer valor, el espacio en el búfer se libera y el productor puede finalmente entregar el 3. Este es el núcleo del desacoplamiento: el productor no tiene que esperar a que el trabajo se complete, solo a que haya un hueco disponible en la cola.
Finalmente, usamos done := make(chan bool) como un canal sin búfer para la señalización. Al no tener búfer, la línea done <- true en el consumidor y <-done en el main actúan como un punto de encuentro exacto. El main no seguirá adelante hasta que el consumidor esté listo para entregar la señal, garantizando que todo el trabajo se haya procesado antes de salir del programa.
El error frecuente
Un error clásico es intentar usar un canal sin búfer para “enviar” resultados desde una goroutine hacia el hilo principal sin haber iniciado una lectura previa, lo que bloquea la goroutine para siempre.
func errorCase() {
// Canal sin búfer
ch := make(chan int)
// Error: Intentar enviar en la misma goroutine sin un receptor activo
// provocará un deadlock inmediato.
ch <- 42
// En una goroutine:
go func() {
ch <- 42 // Esta goroutine se queda bloqueada eternamente si no hay un receptor.
}()
}
Si la goroutine que envía se bloquea y nunca llega a terminar, se produce una goroutine leak (fuga de goroutines), consumiendo memoria de forma indefinida.
N° 134