Los canales son el mecanismo de comunicación entre goroutines en Go. La filosofía del lenguaje los resume así: no te comuniques compartiendo memoria; comparte memoria comunicándote. Un canal es un conducto tipado por el que se pasan valores de una goroutine a otra de forma segura y sincronizada.
Crear un canal y hacer el primer envío
package main
import "fmt"
func main() {
ch := make(chan int) // canal sin buffer (síncrono)
go func() {
ch <- 42 // envía el valor; bloquea hasta que alguien lo lea
}()
v := <-ch // recibe; bloquea hasta que haya un valor
fmt.Println(v) // 42
}
El envío ch <- valor bloquea la goroutine emisora hasta que otra goroutine reciba. La recepción <-ch bloquea hasta que haya algo disponible. Si ninguna goroutine puede avanzar, el runtime detecta el deadlock:
fatal error: all goroutines are asleep - deadlock!
Buffered channels: desacoplar emisor y receptor
Con un canal con buffer puedes enviar hasta n valores sin que nadie los lea de inmediato. El envío solo bloquea cuando el buffer está lleno:
ch := make(chan string, 3) ch <- "primero" ch <- "segundo" ch <- "tercero" // ch <- "cuarto" // deadlock: buffer lleno fmt.Println(<-ch) // "primero" (FIFO) fmt.Println(<-ch) // "segundo"
Cerrar un canal con close()
El emisor cierra el canal cuando ya no tiene más valores que enviar. Recibir de un canal cerrado devuelve el zero value del tipo más el valor false en la forma de dos valores:
ch := make(chan int, 5)
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
// forma de dos valores para detectar cierre
for {
v, ok := <-ch
if !ok {
break // canal cerrado y vaciado
}
fmt.Println(v)
}
range sobre canal: la forma más limpia
El bucle for range sobre un canal lee hasta que se cierra, lo que simplifica el patrón anterior:
func generador(hasta int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < hasta; i++ {
ch <- i
}
close(ch) // sin esto, range se bloquea para siempre
}()
return ch
}
func main() {
for v := range generador(5) {
fmt.Println(v) // 0 1 2 3 4
}
}
Canales direccionales: restringir el uso
Puedes declarar canales que solo permiten enviar o solo permiten recibir. Es una forma de documentar y forzar en el compilador el rol de cada función:
func productor(out chan<- int) { // solo puede enviar
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consumidor(in <-chan int) { // solo puede recibir
for v := range in {
fmt.Println(v)
}
}
func main() {
ch := make(chan int, 5)
go productor(ch)
consumidor(ch)
}
Patrón pipeline
Los canales permiten construir pipelines donde cada etapa transforma los datos y los pasa a la siguiente:
func cuadrados(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for v := range in {
out <- v * v
}
close(out)
}()
return out
}
func main() {
numeros := generador(5) // 0 1 2 3 4
cuad := cuadrados(numeros)
for v := range cuad {
fmt.Println(v) // 0 1 4 9 16
}
}
