Enviar y recibir
Un channel es una tubería tipada entre goroutines. ch <- v envía, <-ch recibe. Se crea con make:
El valor cero de un tipo channel es nil, así que var ch chan string sin make te da un channel que se bloquea para siempre. Crea siempre los channels con make.
Los channels sin buffer sincronizan
make(chan T) crea un channel sin buffer. Un envío se bloquea hasta que un receptor toma el valor, y una recepción se bloquea hasta que un emisor lo proporciona. Las dos goroutines se encuentran en ese punto, lo que convierte a un channel sin buffer en una herramienta de sincronización tanto como en una tubería de datos. Cuando <-done retorna en el siguiente ejemplo, sabes que el worker terminó todo lo que hizo antes de su envío:
Leer result en main es seguro aquí sin un mutex. El modelo de memoria de Go garantiza que todo lo que hizo el worker antes del envío es visible para main después de la recepción correspondiente. chan struct{} es el tipo idiomático para una señal pura, porque struct{} no ocupa memoria.
Channels con buffer
make(chan T, n) da al channel espacio para n valores. Los envíos tienen éxito sin receptor hasta que el buffer se llena; las recepciones, hasta que se vacía. Los valores salen en el mismo orden en que entraron.
Un buffer desacopla emisor y receptor para que las ráfagas cortas no frenen al emisor. No arregla un productor que es siempre más rápido que su consumidor; solo retrasa el momento en que el emisor se bloquea. Elige el tamaño del buffer por una razón (el número de emisores, un tamaño de lote conocido), no para que desaparezca un deadlock.
len(ch) es una instantánea. Para cuando actúas sobre ella otra goroutine puede haberla cambiado, así que no la uses para decidir si un envío se va a bloquear. Para eso usa select con un caso default.
Close y range
close(ch) indica a los receptores que no se enviarán más valores. Después de un close:
- los valores que ya están en el buffer se siguen entregando,
- después, cada recepción devuelve el valor cero de inmediato,
v, ok := <-chinformaok == false,for v := range chtermina.
La primera recepción después de close todavía obtiene "last" con ok == true. La segunda obtiene el valor cero "" y false.
Reglas que provocan panics:
- enviar a un channel cerrado provoca un panic con
send on closed channel, - cerrar un channel ya cerrado provoca un panic,
- cerrar un channel
nilprovoca un panic.
Así que solo cierra el lado emisor, y una sola vez. Con varios emisores, ninguno sabe cuándo han terminado los demás; haz que una goroutine aparte espere a todos los emisores (con un sync.WaitGroup) y cierre el channel cuando Wait retorne. Cerrar solo hace falta cuando un receptor espera el final. Un channel sin cerrar al que nadie hace referencia se recolecta como cualquier otro valor.
Tipos direccionales
Una función puede declarar que solo envía o solo recibe en un channel. El compilador rechaza entonces la otra operación.
| Tipo | Significado | Permitido |
|---|---|---|
chan T | bidireccional | enviar, recibir, cerrar |
chan<- T | solo envío | enviar, cerrar |
<-chan T | solo recepción | recibir |
Un chan T se convierte implícitamente a cualquiera de los tipos restringidos al pasarlo a una función. Por eso produce puede devolver un <-chan int: quien lo llama puede recorrerlo con range pero no enviar ni cerrarlo. Usa tipos direccionales en cada parámetro de función donde apliquen. Documentan quién es el dueño y convierten el mal uso en un error de compilación.
Deadlock
Si todas las goroutines están bloqueadas y nada puede despertar a ninguna, el runtime aborta el programa:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
El volcado de goroutines te dice qué operación está atascada (aquí chan send, o también chan receive, sync.WaitGroup.Wait, select). Las causas habituales:
- un envío a un channel sin buffer sin ningún receptor en marcha,
rangesobre un channel que nunca se cierra,- un contador de
WaitGroupque nunca llega a cero, - dos goroutines esperando cada una a la otra.
El runtime solo detecta el caso en que todas las goroutines están atascadas. En un servidor con otras goroutines vivas (un listener HTTP, un ticker), el mismo bug no hace caer nada; la goroutine bloqueada simplemente se fuga.
Channels nil
Los envíos y recepciones en un channel nil se bloquean para siempre. Parece inútil, pero es un truco estándar dentro de select: asignar nil a una variable de channel desactiva su caso. Este código combina dos channels y deja de escuchar cada uno cuando se cierra:
Sin las asignaciones a nil, un channel cerrado siempre está listo, y el bucle daría vueltas sobre valores cero.
Un pipeline
Los channels se componen en pipelines: cada etapa es una goroutine que recibe de un channel, envía al siguiente y cierra su salida cuando termina su entrada.
Cada etapa se ejecuta de forma concurrente, y el orden de salida es determinista (1, 16, 81) porque cada etapa es una sola goroutine que conserva el orden. Los cierres se encadenan: generate cierra, lo que termina el range de square, que cierra su salida, y así hasta main.
El punto débil de este pipeline: si main dejara de leer antes de tiempo, las etapas se bloquearían en sus envíos para siempre. Los pipelines reales reciben un context.Context o un channel done y hacen select sobre él junto a cada envío.
Channel o mutex
Los channels sirven para pasar la propiedad de los datos y para señalar eventos. Un sync.Mutex es más sencillo para proteger estado compartido que muchas goroutines leen y actualizan en el sitio, como una caché o un contador. Un struct con un mutex dentro suele ser más claro que una goroutine que posee el estado y atiende peticiones por channels. Usa lo que haga el código más corto y la propiedad más evidente.
Referencia rápida
| Operación | channel nil | channel abierto | channel cerrado |
|---|---|---|---|
ch <- v | se bloquea para siempre | se bloquea hasta que se recibe o hay sitio en el buffer | panic |
<-ch | se bloquea para siempre | se bloquea hasta que hay un valor | valores del buffer, luego valor cero |
v, ok := <-ch | se bloquea para siempre | ok es true | ok es false al vaciarse |
close(ch) | panic | cierra | panic |
len(ch), cap(ch) | 0, 0 | valores en el buffer, tamaño del buffer | valores restantes, tamaño del buffer |
Preguntas frecuentes
¿Qué diferencia hay entre un channel con buffer y uno sin buffer en Go?
Un channel sin buffer (make(chan int)) no tiene almacenamiento: un envío se bloquea hasta que otra goroutine recibe, así que cada envío es también una entrega y un punto de sincronización. Un channel con buffer (make(chan int, 3)) guarda hasta 3 valores; los envíos se bloquean solo cuando el buffer está lleno y las recepciones solo cuando está vacío.
¿Qué pasa al leer de un channel cerrado en Go?
Las recepciones en un channel cerrado nunca se bloquean. Primero vacían los valores que queden en el buffer y después devuelven el valor cero del tipo del elemento para siempre. Usa v, ok := <-ch para distinguirlo: ok es false cuando el channel está cerrado y vacío. Un bucle for v := range ch termina en ese momento.
¿Quién debe cerrar un channel en Go?
El emisor, y solo cuando los receptores necesitan saber que no llegarán más valores (por ejemplo, para terminar un bucle range). Enviar a un channel cerrado provoca un panic, y cerrar un channel dos veces también, así que si un receptor cierra el channel compite con los emisores. No hace falta cerrar un channel para liberarlo; el recolector de basura recupera los channels inalcanzables de todos modos.
¿Qué significa "fatal error: all goroutines are asleep"?
Todas las goroutines del programa están bloqueadas en una operación de channel o en un lock que nada puede completar, así que el runtime detiene el programa. La causa más común es enviar a un channel sin buffer en main sin ninguna otra goroutine recibiendo, o recorrer con range un channel que nunca se cierra.