Una goroutine es una función que Go ejecuta de forma concurrente con otras goroutines. Se lanza con la palabra clave go delante de una llamada a función y consume apenas 2-8 KB de stack inicial, lo que hace posible tener cientos de miles activas simultáneamente. Pero tener muchas goroutines activas no significa que trabajen bien juntas: las race conditions son el error más común y el detector de carreras integrado en Go las encuentra.
Lanzar una goroutine
package main
import (
"fmt"
"time"
)
func saludar(nombre string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hola, %s (%d)n", nombre, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go saludar("Ana") // goroutine concurrente
go saludar("Luis") // goroutine concurrente
saludar("main") // ejecuta en la goroutine principal
time.Sleep(500 * time.Millisecond) // espera rudimentaria
}
Sin la espera al final, main() terminaría antes de que las goroutines secundarias pudieran imprimir nada. La forma correcta de esperar es con canales o sync.WaitGroup.
El scheduler M:N de Go
Go usa un scheduler propio que mapea N goroutines sobre M OS threads. El número de threads se controla con GOMAXPROCS, que por defecto es el número de núcleos de la máquina. Esto significa que Go puede ejecutar goroutines en paralelo real en hardware multinúcleo, y además intercalarlas de forma cooperativa cuando se bloquean en operaciones de I/O.
import "runtime"
func main() {
fmt.Println("CPUs:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 0 = consultar sin cambiar
}
Race condition: el error silencioso
Una race condition ocurre cuando dos goroutines acceden a la misma variable al mismo tiempo y al menos una escribe. El resultado es indeterminado:
var contador int
func incrementar() {
for i := 0; i < 1000; i++ {
contador++ // lectura + escritura sin protección
}
}
func main() {
go incrementar()
go incrementar()
time.Sleep(time.Second)
fmt.Println(contador) // podría ser 1247, 1893, 2000 o cualquier número
}
Detectar carreras con go run -race
El detector de carreras de Go instrumenta el código para detectar accesos concurrentes no sincronizados. Actívalo durante el desarrollo y en el CI:
go run -race main.go go test -race ./...
La salida indica exactamente qué goroutines acceden a qué dirección de memoria y en qué línea:
==================
WARNING: DATA RACE
Write at 0x000001234568 by goroutine 7:
main.incrementar()
/tmp/main.go:9 +0x28
Read at 0x000001234568 by goroutine 8:
main.incrementar()
/tmp/main.go:9 +0x20
Goroutines anónimas
Es habitual lanzar goroutines con funciones anónimas, pero hay un error clásico en bucles:
// MAL: todas las goroutines capturan la misma variable i
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // probablemente imprime 5 cinco veces
}()
}
// BIEN: pasa i como parámetro para capturarlo por valor
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // 0 1 2 3 4 (en orden arbitrario)
}(i)
}
Cuántas goroutines puedes tener
func main() {
const N = 100_000
done := make(chan struct{}, N)
for i := 0; i < N; i++ {
go func() {
time.Sleep(time.Second)
done <- struct{}{}
}()
}
for i := 0; i < N; i++ {
<-done
}
fmt.Println("100.000 goroutines completadas")
}
En un portátil con 8 GB de RAM, 100.000 goroutines consumen unos 200-800 MB de stack. Comparable a tener 100.000 objetos de tamaño modesto; algo imposible con threads nativos del sistema operativo.
