Uso de channels para comunicación entre goroutines

Lectura
25 min~5 min lectura

Concepto clave

En Go, los channels son tuberías de comunicación que permiten a las goroutines enviar y recibir datos de forma segura y sincronizada. Piensa en ellos como una línea de montaje en una fábrica: cada trabajador (goroutine) recibe piezas por una cinta transportadora (channel), las procesa, y las pasa a la siguiente estación. Esto evita que los trabajadores choquen entre sí o accedan a los mismos recursos al mismo tiempo.

Los channels son fundamentales para el patrón "share memory by communicating" de Go, que prioriza la comunicación sobre el bloqueo mutuo. En APIs de alto rendimiento, esto significa que puedes procesar múltiples solicitudes HTTP simultáneamente sin usar locks costosos, mejorando el rendimiento y evitando condiciones de carrera. Por ejemplo, en un microservicio REST, podrías usar channels para distribuir tareas de procesamiento de datos entre goroutines mientras mantienes el orden y la integridad.

Cómo funciona en la práctica

Para usar channels, primero los creas con make(chan tipo), donde tipo es el dato que transmitirán (como int, string, o estructuras personalizadas). Luego, las goroutines usan el operador <- para enviar o recibir: channel <- valor envía, y valor := <-channel recibe. Por defecto, estas operaciones bloquean hasta que hay un receptor o emisor listo, lo que sincroniza automáticamente las goroutines.

En un escenario de API, imagina que tienes un endpoint que necesita procesar 100 registros de una base de datos. En lugar de hacerlo secuencialmente, lanzas 10 goroutines que leen de un channel con IDs de registros, cada una procesa su registro, y envía el resultado a otro channel. La goroutine principal recoge todos los resultados, reduciendo el tiempo de respuesta. Esto es más eficiente que usar mutexes porque evita la contención y aprovecha los múltiples núcleos del CPU.

Código en acción

Aquí tienes un ejemplo básico de channels en Go, mostrando cómo comunicar entre goroutines:

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d procesando job %d\n", id, job)
        time.Sleep(time.Second) // Simula trabajo
        results <- job * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Lanza 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Envía 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Recoge resultados
    for a := 1; a <= 5; a++ {
        <-results
    }
}

Ahora, un ejemplo más avanzado aplicado a un microservicio REST con gorilla/mux, refactorizando de un enfoque secuencial a uno concurrente:

// Antes: Procesamiento secuencial en un handler
func processDataSequential(w http.ResponseWriter, r *http.Request) {
    data := fetchDataFromDB() // Lento
    processed := []string{}
    for _, item := range data {
        processed = append(processed, transform(item)) // Transformación costosa
    }
    json.NewEncoder(w).Encode(processed)
}

// Después: Usando channels para concurrencia
func processDataConcurrent(w http.ResponseWriter, r *http.Request) {
    data := fetchDataFromDB()
    resultChan := make(chan string, len(data))

    for _, item := range data {
        go func(it DataItem) {
            resultChan <- transform(it)
        }(item)
    }

    processed := []string{}
    for i := 0; i < len(data); i++ {
        processed = append(processed, <-resultChan)
    }
    json.NewEncoder(w).Encode(processed)
}

Errores comunes

  • Deadlocks por no cerrar channels: Si olvidas cerrar un channel con close() cuando ya no envías datos, las goroutines que leen con range pueden bloquearse indefinidamente. Siempre cierra el channel en el productor cuando termines.
  • Panic por enviar a un channel cerrado: Enviar datos a un channel cerrado causa panic. Asegúrate de que la lógica de cierre esté coordinada, por ejemplo, usando un channel de control o contextos.
  • Uso excesivo de channels buffered: Los channels con buffer (ej., make(chan int, 100)) pueden mejorar el rendimiento, pero si el buffer es muy grande, podrías consumir memoria innecesaria o enmascarar problemas de sincronización. Usa buffers pequeños (10-100) para APIs.
  • Ignorar el patrón worker pool: Lanzar una goroutine por cada tarea puede saturar el sistema. En su lugar, usa un worker pool con un número fijo de goroutines y un channel de jobs, como en el primer ejemplo.
  • No manejar timeouts: En APIs, las operaciones con channels deben tener timeouts para evitar que solicitudes se bloqueen para siempre. Usa select con time.After o contextos con deadline.

Checklist de dominio

  1. Puedo crear channels buffered y unbuffered, y explicar cuándo usar cada uno.
  2. Sé implementar un worker pool con channels para procesar tareas concurrentemente en un handler HTTP.
  3. Uso close() en channels correctamente para evitar deadlocks.
  4. Aplico select con channels para manejar múltiples operaciones o timeouts en APIs.
  5. Evito condiciones de carrera usando channels en lugar de variables compartidas con mutexes.
  6. Mido el rendimiento de mi código concurrente con herramientas como pprof para ajustar buffers y número de workers.
  7. Documento el flujo de datos entre goroutines en mi microservicio para mantener la claridad del código.

Implementa un procesador concurrente de solicitudes HTTP con channels

En este ejercicio, crearás un handler para un microservicio REST que procese múltiples solicitudes de datos usando channels y goroutines. Sigue estos pasos:

  1. Crea un nuevo proyecto Go con gorilla/mux. Define un endpoint POST /process que reciba un JSON con una lista de IDs (ej., {"ids": [1,2,3,4,5]}).
  2. Implementa una función fetchData(id int) string que simule una llamada lenta a una base de datos (usa time.Sleep(100 * time.Millisecond)).
  3. En el handler, lanza una goroutine por cada ID para llamar a fetchData concurrentemente. Usa un channel buffered para recolectar los resultados.
  4. Asegúrate de que todas las goroutines terminen antes de responder. Usa un sync.WaitGroup o cierra el channel y usa un bucle range.
  5. Devuelve los resultados como un JSON array en la respuesta HTTP. Prueba con 10 IDs y verifica que el tiempo total sea menor que el secuencial (1 segundo vs. 100ms * 10).
  6. Opcional: Añade un timeout de 500ms usando select para cancelar operaciones lentas y devolver un error parcial.
Pistas
  • Usa un channel de tipo chan string con buffer igual al número de IDs para evitar bloqueos.
  • Para esperar a las goroutines, considera usar defer wg.Done() en cada goroutine y wg.Wait() antes de cerrar el channel.
  • En el timeout, recuerda que time.After devuelve un channel; úsalo en un select dentro del worker.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.