Dominando la comunicación con canales en Go

Un channel es un conducto tipado diseñado para la transferencia segura de datos entre goroutines. Su diseño permite que la comunicación sea el mecanismo de sincronización, evitando la necesidad de usar bloqueos explícitos (como sync.Mutex) para proteger la memoria compartida. En lugar de que múltiples hilos intenten manipular la misma variable, un valor “viaja” a través del canal de un emisor a un receptor.

Esta arquitectura funciona porque el runtime de Go gestiona la semántica de bloqueo: si un canal es unbuffered [disponible desde Go 1.0], el emisor se detiene hasta que un receptor esté listo para recibirlo; si es buffered (con búfer), como en make(chan T, capacidad), el emisor solo se bloquea cuando el búfer está lleno. Debes usar canales para orquestar flujos de datos, pipelines de procesamiento o señales de terminación. Si los usas mal —por ejemplo, intentando enviar datos a un canal que ya ha sido cerrado o esperando un valor de un canal que nunca se enviará— provocarás un panic o un deadlock que detendrá tu aplicación por completo.

package main

import (
	"fmt"
	"sync"
	"time"
)

type Job struct {
	ID    int
	Value int
}

type Result struct {
	JobID int
	Sum   int
}

// worker procesa trabajos recibidos por el canal 'jobs'
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done() // Informa al WaitGroup que este worker terminó
	for job := range jobs {
		fmt.Printf("Worker %d: procesando job %d\n", id, job.ID)
		// Simulamos una tarea que toma tiempo
		time.Sleep(time.Millisecond * 50)
		results <- Result{JobID: job.ID, Sum: job.Value * 2}
	}
}

func main() {
	const numJobs = 5
	const numWorkers = 2

	// Canal unbuffered: el emisor se bloquea hasta que un worker esté listo
	jobs := make(chan Job)
	// Canal buffered: permite que los workers entreguen resultados sin esperar al main
	results := make(chan Result, numJobs)

	var wg sync.WaitGroup

	// Lanzamos el pool de workers
	for w := 1; w <= numWorkers; w++ {
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}

	// Enviamos los trabajos al canal
	for j := 1; j <= numJobs; j++ {
		jobs <- Job{ID: j, Value: j * 10}
	}

	// Cerramos 'jobs' para que el 'range' dentro de los workers termine
	close(jobs)

	// Esta goroutine es vital: espera a que todos los workers terminen
	// para poder cerrar el canal de resultados de forma segura.
	go func() {
		wg.Wait()
		close(results)
	}()

	// El recolector lee los resultados hasta que 'results' se cierra
	for res := range results {
		fmt.Printf("Resultado recibido: Job %d -> %d\n", res.JobID, res.Sum)
	}

	fmt.Println("Proceso completado.")
}

En el ejemplo anterior, hemos implementado un patrón de Worker Pool robusto. Primero, observamos cómo jobs := make(chan Job) crea un canal unbuffered; esto obliga al main a esperar a que un worker esté disponible para recibir la tarea, proporcionando un control de flujo natural. Por el contrario, results := make(chan Result, numJobs) es un canal buffered que permite a los workers “soltar” su resultado y seguir con el siguiente trabajo sin esperar a que el hilo principal lo lea.

Para evitar que el programa se quede esperando eternamente (deadlock), utilizamos un sync.WaitGroup (una herramienta de sincronización para esperar grupos de goroutines). Es crucial la goroutine que ejecuta wg.Wait() y luego close(results): si no cerramos el canal results, el for res := range results en el main se quedaría bloqueado esperando más datos para siempre. Además, el uso de close(jobs) es lo que permite que el for job := range jobs dentro de la función worker sepa que ya no habrá más trabajo y finalice su ejecución limpiamente.

El error frecuente

Un error clásico en producción es intentar enviar un dato a un canal que ya ha sido cerrado. Esto no es un error de compilación, sino un error en tiempo de ejecución que provoca un panic.

ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

Esto ocurre frecuentemente cuando la lógica de cierre de un canal no está bien coordinada con los productores. La regla de oro es: el emisor es el responsable de cerrar el canal, y nunca debe cerrarse un canal si hay una posibilidad de que algún goroutine todavía intente enviar datos en él.

133

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio