# Channels en Go: Buffered vs Unbuffered - Comunicación Segura entre Goroutines

En el post anterior exploramos las **goroutines** y cómo crear concurrencia ligera en Go. Hoy vamos a descubrir cómo las goroutines se comunican de forma segura: los **channels**. Si vienes de Java, los channels son como `BlockingQueue`, pero integrados directamente en el lenguaje como primitivos de primera clase.

## **¿Qué son los Channels?**

Los channels son el mecanismo de comunicación entre goroutines en Go. Son similares a las colas bloqueantes en Java (`BlockingQueue`), pero con una diferencia crucial: están diseñados específicamente para la comunicación segura entre goroutines y previenen race conditions de forma natural.

### **Comparación: Java vs Go**

| Aspecto | Java | Go |
| --- | --- | --- |
| Comunicación entre threads | `BlockingQueue`, `ConcurrentLinkedQueue` | Channels (primitivo del lenguaje) |
| Sincronización | Locks, semáforos, synchronized | Integrada en channels |
| Seguridad de tipos | Genéricos | Tipado estático |
| Bloqueo | Explícito | Automático |
| Cierre | No necesario | `close()` explícito |

## **Channels Unbuffered: Sincronización Garantizada**

Los channels **unbuffered** (sin buffer) son la forma más básica de comunicación. Tienen una característica importante: **bloquean hasta que hay un receptor listo**.

```go
// Crear un channel unbuffered
ch := make(chan string)

// Goroutine que envía
go func() {
    fmt.Println("Sending message...")
    ch <- "Hello from goroutine"  // ← Bloquea hasta que alguien reciba
    fmt.Println("Message sent!")  // ← Solo se ejecuta después de recibir
}()

// Esperar un poco para demostrar el bloqueo
time.Sleep(500 * time.Millisecond)

// Recibir mensaje (desbloquea el sender)
msg := <-ch
fmt.Printf("Received: %s\n", msg)
```

**Características clave:**

* ✅ **Sincronización garantizada**: El sender y receiver se sincronizan automáticamente
    
* ✅ **Comunicación directa**: El mensaje se transfiere directamente sin buffer intermedio
    
* ✅ **Bloqueo mutuo**: El sender bloquea hasta que hay un receiver, y viceversa
    

### **¿Cuándo Usar Channels Unbuffered?**

Los channels unbuffered son perfectos cuando:

* Necesitas sincronización entre goroutines
    
* Quieres garantizar que el mensaje fue recibido antes de continuar
    
* Necesitas comunicación punto a punto (1:1)
    

## **Channels Buffered: Capacidad y Rendimiento**

Los channels **buffered** tienen una capacidad fija. Solo bloquean cuando están **llenos** (sender) o **vacíos** (receiver).

```go
// Crear un channel con buffer de tamaño 3
ch := make(chan int, 3)

// Enviar múltiples valores sin bloqueo (hasta que el buffer esté lleno)
ch <- 1
ch <- 2
ch <- 3
fmt.Println("Sent 3 values to buffered channel")

// El siguiente envío bloquearía porque el buffer está lleno
// ch <- 4  // ← Esto bloquearía

// Recibir valores
fmt.Printf("Received: %d\n", <-ch)  // Libera espacio en el buffer
fmt.Printf("Received: %d\n", <-ch)
fmt.Printf("Received: %d\n", <-ch)
```

**Características clave:**

* ✅ **No bloquea inmediatamente**: Puedes enviar hasta que el buffer esté lleno
    
* ✅ **Mejor rendimiento**: Reduce la sincronización cuando hay desajustes de velocidad
    
* ✅ **Desacoplamiento temporal**: El sender y receiver pueden trabajar a diferentes velocidades
    

### **¿Cuándo Usar Channels Buffered?**

Los channels buffered son ideales cuando:

* Tienes un productor rápido y un consumidor lento (o viceversa)
    
* Quieres reducir el bloqueo entre goroutines
    
* Necesitas un buffer para manejar picos de carga
    
* Estás implementando un worker pool
    

## **Iterando sobre Channels con** `range`

Puedes iterar sobre un channel hasta que se cierre, similar a iterar sobre una colección:

```go
ch := make(chan int)

// Goroutine que envía valores y cierra el channel
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
    close(ch)  // ← Importante: cerrar el channel cuando terminamos
}()

// Iterar sobre el channel
for value := range ch {
    fmt.Printf("Received: %d\n", value)
}
fmt.Println("Channel closed")
```

**Puntos importantes:**

* ✅ Solo el **sender** debe cerrar el channel
    
* ✅ Cerrar un channel cerrado causa panic
    
* ✅ Recibir de un channel cerrado devuelve el zero value y `false`
    
* ✅ `range` termina automáticamente cuando el channel se cierra
    

### **Verificar si un Channel está Cerrado**

```go
value, ok := <-ch
if !ok {
    fmt.Println("Channel is closed")
    return
}
fmt.Printf("Received: %d\n", value)
```

## **Channels Direccionales: Solo Enviar o Solo Recibir**

Go permite especificar si un channel solo envía (`chan<-`) o solo recibe (`<-chan`). Esto mejora la seguridad de tipos y hace el código más claro:

```go
// Función que solo envía (chan<-)
sender := func(ch chan<- string) {
    ch <- "Hello"
    ch <- "World"
    close(ch)
    // <-ch  // ← Error de compilación: no puedes recibir
}

// Función que solo recibe (<-chan)
receiver := func(ch <-chan string) {
    for msg := range ch {
        fmt.Printf("Received: %s\n", msg)
    }
    // ch <- "test"  // ← Error de compilación: no puedes enviar
}

ch := make(chan string)
go sender(ch)
receiver(ch)
```

**Ventajas:**

* ✅ **Seguridad de tipos**: Previene errores de uso incorrecto
    
* ✅ **Documentación**: Hace explícito el propósito del channel
    
* ✅ **Mejor diseño**: Separa responsabilidades claramente
    

## **Select Statement: Esperar en Múltiples Channels**

El `select` statement permite esperar en múltiples channels simultáneamente, similar a `Selector` en Java NIO pero mucho más simple:

```go
ch1 := make(chan string)
ch2 := make(chan string)

// Goroutine que envía a ch1 después de 1 segundo
go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "Message from ch1"
}()

// Goroutine que envía a ch2 después de 2 segundos
go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "Message from ch2"
}()

// Select espera en ambos channels
// Ejecuta el primer case que esté listo
select {
case msg1 := <-ch1:
    fmt.Printf("Received from ch1: %s\n", msg1)
case msg2 := <-ch2:
    fmt.Printf("Received from ch2: %s\n", msg2)
}
```

**Características:**

* ✅ Ejecuta el **primer case** que esté listo
    
* ✅ Si múltiples cases están listos, elige uno **aleatoriamente**
    
* ✅ Bloquea hasta que al menos un case esté listo
    

### **Select con Default: Non-Blocking**

El `default` case hace que `select` no bloquee:

```go
ch := make(chan string)

// Intentar recibir sin bloquear
select {
case msg := <-ch:
    fmt.Printf("Received: %s\n", msg)
default:
    fmt.Println("No message available (non-blocking)")
}

// Enviar sin bloquear (solo funciona con buffered channels)
bufferedCh := make(chan string, 1)
select {
case bufferedCh <- "Hello":
    fmt.Println("Sent to buffered channel")
default:
    fmt.Println("Channel full, couldn't send")
}
```

### **Select con Timeout**

Usar `time.After` para implementar timeouts:

```go
ch := make(chan string)

// Goroutine que tarda mucho
go func() {
    time.Sleep(3 * time.Second)
    ch <- "Slow message"
}()

// Esperar con timeout de 1 segundo
select {
case msg := <-ch:
    fmt.Printf("Received: %s\n", msg)
case <-time.After(1 * time.Second):
    fmt.Println("Timeout! No message received")
}
```

## **Ejemplo Práctico: Worker Pool**

Los worker pools son un patrón común donde múltiples workers procesan trabajos de una cola:

```go
func workerPoolExample() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)

    // Crear 3 workers
    for w := 1; w <= 3; w++ {
        go func(id int) {
            for job := range jobs {
                fmt.Printf("Worker %d processing job %d\n", id, job)
                time.Sleep(500 * time.Millisecond) // Simular trabajo
                results <- job * 2
            }
        }(w)
    }

    // Enviar trabajos
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)  // ← Cerrar cuando no hay más trabajos

    // Recibir resultados
    for r := 1; r <= 5; r++ {
        result := <-results
        fmt.Printf("Result: %d\n", result)
    }
}
```

**Características del patrón:**

* ✅ **Channel de jobs**: Buffered para permitir envío sin bloqueo
    
* ✅ **Múltiples workers**: Procesan trabajos concurrentemente
    
* ✅ **Cerrar jobs**: Cuando no hay más trabajos, cierra el channel
    
* ✅ **Workers terminan**: `range` termina cuando el channel se cierra
    

## **Ejemplo Avanzado: Fan-Out / Fan-In Pattern**

El patrón Fan-Out/Fan-In distribuye trabajo entre múltiples workers y luego combina los resultados:

**Fan-Out**: Múltiples workers procesan de un channel **Fan-In**: Combinar resultados de múltiples channels en uno

```go
func fanOutFanIn() {
    // Channel de entrada
    input := make(chan int)

    // Fan-out: múltiples workers procesan
    worker1 := make(chan int)
    worker2 := make(chan int)

    go func() {
        for val := range input {
            worker1 <- val * 2
        }
        close(worker1)
    }()

    go func() {
        for val := range input {
            worker2 <- val * 3
        }
        close(worker2)
    }()

    // Fan-in: combinar resultados usando select
    output := make(chan int)
    go func() {
        for {
            select {
            case val, ok := <-worker1:
                if !ok {
                    worker1 = nil  // Marcar como cerrado
                } else {
                    output <- val
                }
            case val, ok := <-worker2:
                if !ok {
                    worker2 = nil  // Marcar como cerrado
                } else {
                    output <- val
                }
            }
            // Terminar cuando ambos están cerrados
            if worker1 == nil && worker2 == nil {
                close(output)
                return
            }
        }
    }()

    // Enviar datos
    go func() {
        for i := 1; i <= 5; i++ {
            input <- i
        }
        close(input)
    }()

    // Recibir resultados combinados
    for result := range output {
        fmt.Printf("Result: %d\n", result)
    }
}
```

Este patrón es útil cuando:

* Tienes múltiples workers procesando datos
    
* Necesitas combinar resultados de diferentes fuentes
    
* Quieres paralelizar procesamiento pesado
    

## **Comparación: Unbuffered vs Buffered**

| Aspecto | Unbuffered | Buffered |
| --- | --- | --- |
| Sincronización | Garantizada | No garantizada |
| Bloqueo | Inmediato | Solo cuando está lleno/vacío |
| Uso de memoria | Mínimo | Mayor (buffer) |
| Velocidad | Más lento (más sincronización) | Más rápido (menos bloqueo) |
| Casos de uso | Sincronización, 1:1 | Worker pools, productor-consumidor |

### **Regla General**

* **Usa unbuffered** cuando necesitas sincronización garantizada
    
* **Usa buffered** cuando quieres mejorar el rendimiento y tienes desajustes de velocidad
    

## **Mejores Prácticas**

### **1\. Siempre Cierra Channels desde el Sender**

```go
// ✅ BIEN: El sender cierra el channel
go func() {
    defer close(ch)  // ← Usar defer para asegurar cierre
    for i := 0; i < 10; i++ {
        ch <- i
    }
}()

// ❌ MAL: El receiver no debe cerrar
func receiver(ch <-chan int) {
    for val := range ch {
        fmt.Println(val)
    }
    // close(ch)  // ← Error: receiver no puede cerrar
}
```

### **2\. Usa Buffered Channels para Worker Pools**

```go
// ✅ BIEN: Buffer permite enviar trabajos sin bloqueo
jobs := make(chan Job, 100)

// ❌ MAL: Unbuffered puede causar bloqueo innecesario
jobs := make(chan Job)  // ← Workers deben estar listos inmediatamente
```

### **3\. Evita Leaks: Siempre Recibe o Cierra**

```go
// ❌ MAL: Goroutine bloqueada para siempre
go func() {
    ch <- "message"  // ← Bloquea si nadie recibe
}()

// ✅ BIEN: Asegurar que alguien reciba
go func() {
    ch <- "message"
}()
msg := <-ch  // ← Recibir el mensaje
```

### **4\. Usa Select para Timeouts**

```go
// ✅ BIEN: Timeout para evitar bloqueo indefinido
select {
case result := <-ch:
    return result
case <-time.After(5 * time.Second):
    return errors.New("timeout")
}
```

### **5\. Channels Direccionales para Claridad**

```go
// ✅ BIEN: Hace explícito el propósito
func processData(input <-chan Data, output chan<- Result) {
    // ...
}

// ❌ MENOS CLARO: No es obvio quién envía/recibe
func processData(input chan Data, output chan Result) {
    // ...
}
```

## **Errores Comunes**

### **❌ Error 1: Enviar a un Channel Cerrado**

```go
ch := make(chan int)
close(ch)
ch <- 1  // ← Panic: send on closed channel
```

**Solución**: Solo el sender debe cerrar, y solo cuando ya no enviará más.

### **❌ Error 2: Olvidar Cerrar Channels**

```go
// ❌ MAL: range nunca termina
go func() {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    // Olvidamos close(ch)
}()

for val := range ch {  // ← Bloquea para siempre esperando más valores
    fmt.Println(val)
}
```

**Solución**: Siempre cierra el channel cuando termines de enviar.

### **❌ Error 3: Buffer Demasiado Grande o Pequeño**

```go
// ❌ MAL: Buffer muy pequeño puede causar bloqueo innecesario
ch := make(chan int, 1)  // ← Solo 1 elemento

// ❌ MAL: Buffer muy grande desperdicia memoria
ch := make(chan int, 1000000)  // ← Demasiado grande

// ✅ BIEN: Buffer apropiado para el caso de uso
ch := make(chan int, 10)  // ← Basado en el patrón de carga esperado
```

### **❌ Error 4: No Manejar Cierre en Select**

```go
// ❌ MAL: Puede causar panic si el channel se cierra
select {
case val := <-ch1:
    process(val)
case val := <-ch2:
    process(val)
}

// ✅ BIEN: Verificar si está cerrado
select {
case val, ok := <-ch1:
    if !ok {
        ch1 = nil  // Marcar como cerrado
        continue
    }
    process(val)
case val, ok := <-ch2:
    if !ok {
        ch2 = nil
        continue
    }
    process(val)
}
```

## **Comparación: Java vs Go**

### **Java: BlockingQueue**

```java
// Java
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);

// Producer
new Thread(() -> {
    try {
        queue.put("message");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

// Consumer
new Thread(() -> {
    try {
        String msg = queue.take();
        System.out.println(msg);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();
```

**Problemas:**

* Manejo de excepciones verboso
    
* No hay soporte nativo para múltiples channels (select)
    
* No hay channels direccionales
    
* Más código boilerplate
    

### **Go: Channels**

```go
// Go
ch := make(chan string, 10)

// Producer
go func() {
    ch <- "message"
}()

// Consumer
go func() {
    msg := <-ch
    fmt.Println(msg)
}()
```

**Ventajas:**

* Sintaxis simple y clara
    
* `select` para múltiples channels
    
* Channels direccionales para seguridad
    
* Integrado en el lenguaje
    

## **Conclusiones**

Los channels son el corazón de la comunicación concurrente en Go:

✅ **Sincronización segura**: Previenen race conditions de forma natural

✅ **Flexibilidad**: Unbuffered para sincronización, buffered para rendimiento

✅ **Simplicidad**: Sintaxis clara y expresiva

✅ **Patrones poderosos**: Worker pools, fan-out/fan-in, pipelines

✅ **Integrado**: Parte del lenguaje, no una librería externa

Si vienes de Java, los channels pueden parecer diferentes al principio, pero una vez que entiendas cuándo usar buffered vs unbuffered y cómo usar `select`, verás cómo simplifican enormemente la programación concurrente.

## **Próximos Pasos**

En el siguiente post exploraremos el **context package**, que proporciona cancelación, timeouts y valores de request-scope para goroutines y channels. El context es esencial para escribir código concurrente robusto y cancelable.

---

**¿Has usado channels en tus proyectos de Go?** ¿Prefieres buffered o unbuffered? Comparte tus experiencias y casos de uso en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio [go-mastery-lab](https://github.com/joedayz/go-mastery-lab).
