Concurrencia y multihilo en Go: guía completa para desarrolladores

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Concurrencia y multihilo en Go: de gorutinas a canales

El lenguaje Go (Golang) fue desarrollado en Google teniendo en cuenta los requisitos modernos de rendimiento y escalabilidad. Su principal "característica" es el soporte integrado de concurrencia, que está implementado a nivel del lenguaje, no a través de bibliotecas externas. A diferencia de Python, donde el multihilo está limitado por el GIL (Global Interpreter Lock), Go ofrece gorutinas ligeras que permiten ejecutar miles de tareas simultáneamente de manera eficiente. En este artículo, analizaremos los conceptos clave de concurrencia y multihilo en Go, incluyendo gorutinas, canales, mutex y grupo de espera, con ejemplos prácticos de código.



1. Gorutinas: hilos ligeros de Go

Una gorutina (goroutine) es una función que se ejecuta concurrentemente con otras funciones en el mismo espacio de direcciones. Funciona sobre hilos del sistema, pero utiliza su propio planificador de Go, lo que la hace significativamente más económica que un hilo normal del SO. La creación de una gorutina se realiza con la palabra clave go.



package main

import ( "fmt" "time")

func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}

func main() { go say("Hello") // Inicio de gorutina say("World") // Llamada normal en el hilo principal}


En este ejemplo, say("Hello") se ejecuta en una gorutina separada, mientras que say("World") lo hace en la principal. La salida estará mezclada, ya que ambas funciones trabajan concurrentemente. Es importante recordar: si la función principal main finaliza, todas las gorutinas se detendrán forzosamente.



2. Canales: transferencia segura de datos entre gorutinas

Los canales (channels) son "tuberías" tipificadas a través de las cuales las gorutinas se comunican entre sí. Proporcionan sincronización sin necesidad de usar mutex explícitamente. Los canales pueden ser con búfer (con capacidad) y sin búfer (síncronos).



Canal sin búfer

package main

import "fmt"

func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // envío del resultado al canal}

func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int)

go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)

x, y := <-c, <-c // recepción del canal

fmt.Println(x, y, x+y)}


Aquí, dos gorutinas calculan las sumas de las mitades del array y envían los resultados al canal. La gorutina principal se bloquea hasta recibir ambos valores. Un canal sin búfer funciona como un punto síncrono: el envío se bloquea hasta que el receptor esté listo, y viceversa.



Canal con búfer

package main

import "fmt"

func main() { ch := make(chan int, 2) // búfer de 2 elementos ch <- 1 ch <- 2 // ch <- 3 // esto causaría un deadlock, ya que el búfer está lleno fmt.Println(<-ch) fmt.Println(<-ch)}


Un canal con búfer permite enviar datos sin bloqueo, hasta que el búfer se llene. Esto es útil cuando la velocidad de envío es mayor que la velocidad de procesamiento.



3. Sincronización: WaitGroup y mutex

Aunque los canales cubren la mayoría de los escenarios, a veces se requiere un control más fino sobre la sincronización. Para ello, en el paquete sync existen WaitGroup (espera a que finalice un grupo de gorutinas) y Mutex (exclusión mutua para proteger datos compartidos).



WaitGroup: esperar a todas las gorutinas

package main

import ( "fmt" "sync")

func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // disminuye el contador al finalizar fmt.Printf("Worker %d starting", id) // simulación de trabajo fmt.Printf("Worker %d done", id)}

func main() { var wg sync.WaitGroup

for i := 1; i <= 3; i++ { wg.Add(1) // incrementa

Blogs

Book Recommendations