Introducción a la concurrencia en Go
El lenguaje Go (Golang) fue desarrollado en Google teniendo en cuenta los requisitos modernos de rendimiento y escalabilidad. Una de las características clave de Go es el soporte integrado para concurrencia (concurrency) — la capacidad de un programa para ejecutar múltiples tareas simultáneamente. A diferencia del multihilo tradicional, donde los hilos son gestionados por el sistema operativo, Go ofrece goroutines ligeras y primitivas de sincronización convenientes — canales.
En este artículo analizaremos:
- Qué son las goroutines y cómo ejecutarlas
- Canales para intercambiar datos entre goroutines
- Mutex para acceso seguro a recursos compartidos
- Ejemplos prácticos y patrones de código concurrente
1. Goroutines: hilos ligeros de Go
Una goroutine es una función que se ejecuta concurrentemente con otras goroutines. Ocupa solo unos pocos kilobytes de pila (a diferencia de los hilos del SO, donde la pila puede ser de megabytes). Puedes iniciar una goroutine simplemente añadiendo la palabra clave go antes de la llamada a la función.
Ejemplo: Iniciar goroutines
package main
import ( "fmt" "time")
func sayHello(name string) { for i := 0; i < 3; i++ { fmt.Printf("¡Hola, %s! (iteración %d)", name, i) time.Sleep(100 * time.Millisecond) }}
func main() { go sayHello("Alicia") // Iniciar goroutine go sayHello("Roberto") // Otra goroutine time.Sleep(1 * time.Second) // Esperar a que terminen fmt.Println("Función principal finalizada")}Importante: El programa termina cuando finaliza la función main, incluso si otras goroutines aún están ejecutándose. Por eso usamos time.Sleep para esperar. En código real es mejor usar sync.WaitGroup.
Uso de sync.WaitGroup
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Disminuir el contador al finalizar fmt.Printf("Trabajador %d comenzó a trabajar", id) // ... algún trabajo ... fmt.Printf("Trabajador %d terminó de trabajar", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Incrementar el contador antes de iniciar la goroutine go worker(i, &wg) }
wg.Wait() // Bloquear main hasta que todas las goroutines terminen fmt.Println("Todos los trabajadores han terminado su trabajo")}2. Canales: intercambio de datos entre goroutines
Los canales (channels) son "tuberías" tipadas a través de las cuales las goroutines pueden enviar y recibir valores. Los canales proporcionan sincronización segura sin bloqueos explícitos.
Creación y uso de canales
package main
import "fmt"
func main() { // Crear un canal para enteros ch := make(chan int)
// Iniciar una goroutine que envía datos go func() { ch <- 42 // Enviar valor al canal }()
// Recibir valor del canal value := <-ch fmt.Println("Recibido:", value)}Por defecto, los canales son no almacenados en búfer: el envío se bloquea hasta que el receptor esté listo para recibir los datos, y viceversa. Esto garantiza la sincronización.
Canales con búfer
package main
import "fmt"
func main() { // Canal con búfer para 3 elementos ch := make(chan string, 3)
// Enviar sin bloqueo (mientras el búfer no esté lleno) ch <- "uno" ch <- "dos" ch <- "tres"
// Leer fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)}Cierre de canales y range
package main
import "fmt"
func fibonacci(n int, ch chan int) { a, b := 0, 1 for i := 0; i < n; i++ { ch <-