Un canal en Go es un conducto tipado para enviar valores entre goroutines. Permite coordinar trabajo compartiendo mensajes en lugar de compartir memoria a lo loco.
Ya sabemos lanzar goroutines y esperar a que terminen. Ahora vamos a comunicar valores entre ellas.
En lenguajes como Java o C++, cuando dos hilos necesitan compartir datos, suelen acceder a una variable compartida protegida por un cerrojo (Mutex). Esto es propenso a errores, Race Conditions y Deadlocks complejos.
Go fomenta esta segunda opción con una idea sencilla:
“Do not communicate by sharing memory; instead, share memory by communicating.” (No comuniques compartiendo memoria; comparte memoria comunicando).
Para ello tenemos los canales. Podemos pensarlos como conductos tipados por los que las goroutines envían valores y se sincronizan.
Declaración y sintaxis
Un canal es un tipo de dato como cualquier otro. Se define con la palabra clave chan seguida del tipo de dato que va a viajar por la tubería.
// Declaración (valor cero es nil, ¡cuidado!)
var canalNil chan int
// Inicialización de un canal listo para usar
canal := make(chan int)Enviar o recibir sobre un canal nil bloquea para siempre, y cerrarlo provoca un panic. Este comportamiento permite activar o desactivar casos de un select, pero suele indicar un error fuera de ese patrón.
El operador flecha <-
Para enviar y recibir datos usamos el operador <-. La dirección de la flecha indica el flujo de los datos.
- Enviar datos al canal:
canal <- valor(Metemos el valor en el canal). - Recibir datos del canal:
variable := <-canal(la flecha sale del canal).
Canales sin buffer
Por defecto, los canales en Go no tienen buffer. Esto significa que no tienen “espacio” para guardar datos.
¿Cómo funcionan entonces? Funcionan mediante Sincronización Estricta.
Para que la comunicación se complete, un envío y una recepción deben encontrarse. Una de las goroutines puede haber quedado bloqueada antes de que la otra llegue.
- Si envías (
ch <- 1) y nadie escucha, te bloqueas. - Si intentas recibir (
<- ch) y nadie envía, te bloqueas.
Es como pasar un testigo en una carrera de relevos: ambos corredores deben coincidir en el tiempo y el espacio.
package main
import (
"fmt"
"time"
)
func main() {
// Creamos canal SIN buffer
mensajes := make(chan string)
go func() {
fmt.Println("Goroutine: Enviando mensaje...")
// Esta línea SE BLOQUEA hasta que alguien lea del canal
mensajes <- "¡Ping!"
fmt.Println("Goroutine: Mensaje entregado")
}()
fmt.Println("Main: Esperando un poco...")
time.Sleep(2 * time.Second)
// Al ejecutar esta línea, se desbloquea la goroutine
recibido := <-mensajes
fmt.Println("Main: Recibido:", recibido)
}Los canales sin buffer son perfectos para sincronizar dos goroutines. Garantizan que el intercambio ha ocurrido.
Canales con buffer
A veces no queremos que el emisor se bloquee si el receptor está ocupado. Queremos una “cola” o un buzón donde dejar el mensaje y seguir trabajando.
Esos son los Canales con Buffer. Se crean pasando la capacidad a make.
canal := make(chan string, 3) // Capacidad para 3 mensajes
En un canal con buffer:
- Enviar solo bloquea si el buffer está lleno.
- Recibir solo bloquea si el buffer está vacío.
func main() {
// Canal con capacidad para 2 enteros
cola := make(chan int, 2)
// Podemos enviar sin que nadie escuche (porque hay hueco)
cola <- 1
cola <- 2
fmt.Println("Hemos metido 2 elementos sin bloquearnos")
// cola <- 3 // ¡CUIDADO! Esto bloquearía porque el buffer está lleno (Deadlock en main)
fmt.Println(<-cola) // 1
fmt.Println(<-cola) // 2
}¿Cuándo usar Buffer?
Úsalos para desacoplar productores y consumidores cuando la velocidad de producción/consumo varía. Pero cuidado: un buffer no es memoria infinita. Si el productor es sistemáticamente más rápido que el consumidor, el buffer se llenará y volverás a tener un bloqueo.
Cerrar canales y usar range
La parte productora puede cerrar un canal para indicar que no se enviarán más datos. No es obligatorio cerrar todos los canales: solo hace falta cuando el receptor necesita detectar ese final o liberar un bucle range.
close(canal)
Esto es muy útil para que el receptor sepa cuándo dejar de esperar. De hecho, podemos iterar un canal con range. El bucle range recibe valores continuamente y se rompe automáticamente cuando el canal se cierra.
func productor(c chan int) {
for i := 0; i < 5; i++ {
c <- i
}
close(c) // ¡Importante! Cerramos al terminar
}
func main() {
numeros := make(chan int)
go productor(numeros)
// Range itera hasta que el canal se cierre
for n := range numeros {
fmt.Printf("Recibido: %d\n", n)
}
fmt.Println("Canal cerrado, bucle terminado.")
}Enviar a un canal cerrado o cerrarlo por segunda vez provoca un panic. La responsabilidad de cerrarlo debe estar en el lado productor y, si hay varios emisores, debe coordinarse en un único punto.
También podemos comprobar si un canal sigue abierto con la forma valor, ok := <-canal. Cuando está cerrado y ya no quedan valores pendientes, recibimos el valor cero y ok vale false.
Canales direccionales
Cuando pasamos un canal a una función, podemos especificar si esa función va a usar el canal solo para leer o solo para escribir. Esto mejora la seguridad de tipos (el compilador nos avisa si nos equivocamos).
chan<- Type: Canal solo de escritura (Send-only).<-chan Type: Canal solo de lectura (Receive-only).
// Esta función SOLO puede enviar
func enviar(c chan<- string) {
c <- "Hola"
// mensaje := <-c // ERROR de compilación
}
// Esta función SOLO puede recibir
func recibir(c <-chan string) {
fmt.Println(<-c)
// c <- "Hola" // ERROR de compilación
}Diferencias entre canales con y sin buffer
| Característica | Canal Sin Buffer (make(chan T)) | Canal Con Buffer (make(chan T, N)) |
|---|---|---|
| Comportamiento | Cada envío se sincroniza con una recepción | Los envíos avanzan mientras quede capacidad |
| Envío | Bloquea hasta que alguien lea | Bloquea solo si está lleno |
| Recepción | Bloquea hasta que alguien escriba | Bloquea solo si está vacío |
| Uso principal | Sincronización estricta | Desacople de rendimiento |