Programación asíncrona en Go: conceptos básicos y práctica
Go (Golang) fue diseñado desde el principio como un lenguaje para trabajar eficientemente con tareas concurrentes. A diferencia de muchos otros lenguajes, donde la asincronía se logra mediante callbacks o bibliotecas complejas, Go ofrece herramientas simples y elegantes: goroutines, canales y el operador select. En esta guía, analizaremos cómo funciona la programación asíncrona en Go y mostraremos ejemplos prácticos.
1. Goroutines: hilos de ejecución ligeros
Una goroutine es un hilo de ejecución ligero, gestionado por el runtime de Go. A diferencia de los hilos del sistema, las goroutines tienen una sobrecarga mínima. Puedes ejecutar miles de goroutines sin un impacto significativo en el rendimiento.
Para iniciar una goroutine se utiliza la palabra clave go antes de la llamada a la función:
package main
import ( "fmt" "time")
func printMessage(msg string) { for i := 0; i < 5; i++ { fmt.Println(msg) time.Sleep(100 * time.Millisecond) }}
func main() { // Inicio de goroutine go printMessage("Salida asíncrona") // El hilo principal continúa su trabajo printMessage("Salida síncrona") // Damos tiempo a la goroutine para que termine time.Sleep(1 * time.Second)}Es importante recordar: la función principal main también es una goroutine. Si esta finaliza, todas las demás goroutines serán destruidas. Por eso, a menudo se utiliza sync.WaitGroup o canales para la sincronización.
2. Canales: transferencia segura de datos entre goroutines
Los canales son "tuberías" tipadas a través de las cuales las goroutines pueden intercambiar datos. El principio fundamental: "No se comuniquen a través de la memoria compartida; compartan la memoria a través de la comunicación".
Creación y uso de un canal:
package main
import "fmt"
func main() { // Creamos un canal no buferizado para int ch := make(chan int) // Iniciamos una goroutine que envía datos go func() { for i := 0; i < 5; i++ { ch <- i * 2 } close(ch) // Cerramos el canal después de enviar todos los datos }() // Leemos datos del canal for value := range ch { fmt.Println("Recibido:", value) }}Canales buferizados permiten enviar datos sin necesidad de recibirlos inmediatamente:
ch := make(chan string, 3) // Buffer para 3 elementosch <- "A"ch <- "B"ch <- "C"// Ahora se puede leerfmt.Println(<-ch) // Afmt.Println(<-ch) // B3. Operador select: trabajo con múltiples canales
El operador select permite a una goroutine esperar simultáneamente varias operaciones de canal. Se bloquea hasta que uno de sus casos esté listo para ejecutarse.
Ejemplo con timeout y manejo de múltiples fuentes de datos:
package main
import ( "fmt" "time")
func worker(id int, ch chan string) { time.Sleep(time.Duration(id) * time.Second) ch <- fmt.Sprintf("Resultado del worker %d", id)}
func main() { ch1 := make(chan string) ch2 := make(chan string) go worker(2, ch1) // Finaliza en 2 seg go worker(3, ch2) // Finaliza en 3 seg select { case msg1 := <-ch1: fmt.Println("Primer canal:", msg1) case msg2 := <-ch2: fmt.Println("Segundo canal:", msg2) case <-time.After(1 * time.Second): fmt.Println("¡Timeout! Ningún worker llegó a tiempo") } // Cerramos los canales para evitar fugas close(ch1) close(ch2)}El operador select es ideal para implementar timeouts, operaciones no bloqueantes y el patrón "fan-in" (combinar varios canales en uno).
4. Patrones de concurrencia en Go
Considera