En esta página
Goroutines
Goroutines: concurrencia liviana integrada en el lenguaje
Las goroutines son la característica que hace que Go sea único para programación de sistemas concurrentes. Una goroutine es una función que se ejecuta de forma concurrente con el resto del programa, gestionada por el runtime de Go (no por el sistema operativo directamente).
La diferencia clave con los hilos del sistema operativo es el costo:
- Hilo del SO: 1-8 MB de stack, costoso de crear y destruir
- Goroutine: ~2-4 KB de stack inicial (crece dinámicamente), creación en microsegundos
Un servidor web típico en Go puede manejar cientos de miles de conexiones concurrentes con goroutines, algo imposible con hilos del SO tradicionales.
El scheduler de Go: M:N threading
El runtime de Go implementa un modelo de scheduling M:N: multiplexea M goroutines sobre N hilos del SO. Los parámetros clave son:
- GOMAXPROCS: número de hilos del SO que pueden ejecutar código Go en paralelo (por defecto = número de CPUs)
- El scheduler de Go es cooperativo con preempción: las goroutines ceden el control en puntos específicos (llamadas a funciones, operaciones de I/O, canales)
import "runtime"
fmt.Println("CPUs:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 0 = leer el valor actual
fmt.Println("Goroutines activas:", runtime.NumGoroutine())Lanzar goroutines
La palabra clave go convierte cualquier llamada de función en una goroutine:
// Función normal
func saludar(nombre string) {
fmt.Printf("¡Hola, %s!\n", nombre)
}
func main() {
// Ejecutar en goroutine — retorna inmediatamente
go saludar("Ana")
go saludar("Luis")
// Función anónima en goroutine
go func() {
fmt.Println("Goroutine anónima")
}()
// ¡IMPORTANTE! main() no espera a las goroutines
// Si main() termina, todas las goroutines mueren también
time.Sleep(100 * time.Millisecond) // espera simple (no usar en producción)
}`sync.WaitGroup`: esperar a múltiples goroutines
WaitGroup es el mecanismo estándar para esperar a que un grupo de goroutines termine:
import "sync"
func procesarItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1) // incrementar antes de lanzar la goroutine
go func(i string) {
defer wg.Done() // decrementar al terminar
// procesar item...
fmt.Println("Procesado:", i)
}(item) // pasar item como parámetro — ¡crucial!
}
wg.Wait() // bloquear hasta que el contador sea 0
fmt.Println("Todos los items procesados")
}Error clásico: captura de variable en closure
// INCORRECTO: todas las goroutines capturan la misma variable i
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // i puede ser cualquier valor, incluso 5
}()
}
// CORRECTO: pasar i como parámetro
for i := 0; i < 5; i++ {
go func(id int) {
fmt.Println(id) // cada goroutine tiene su propia copia de id
}(i)
}
// También correcto en Go 1.22+: el loop crea nueva variable i en cada iteración
for i := range 5 {
go func() {
fmt.Println(i) // Go 1.22: i es nueva variable en cada iteración
}()
}Carreras de datos: el peligro de la concurrencia sin sincronización
Una carrera de datos ocurre cuando dos goroutines acceden a la misma memoria concurrentemente y al menos una de ellas escribe, sin ninguna sincronización:
// INCORRECTO: carrera de datos
contador := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contador++ // lectura-modificación-escritura no es atómica
}()
}
wg.Wait()
fmt.Println(contador) // puede ser < 1000 — resultado incorrectoDetectar con el race detector
go run -race main.go
# ==================
# WARNING: DATA RACE
# Write at 0x00c0000b4010 by goroutine 7:
# main.main.func1()
# ...`sync.Mutex`: exclusión mutua
Un Mutex (Mutual Exclusion) garantiza que solo una goroutine pueda ejecutar una sección crítica a la vez:
type Caché struct {
mu sync.Mutex
datos map[string]string
}
func NuevaCaché() *Caché {
return &Caché{datos: make(map[string]string)}
}
func (c *Caché) Set(clave, valor string) {
c.mu.Lock()
defer c.mu.Unlock() // siempre usar defer para garantizar el Unlock
c.datos[clave] = valor
}
func (c *Caché) Get(clave string) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
val, ok := c.datos[clave]
return val, ok
}`sync.RWMutex`: múltiples lectores, escritor único
Cuando las lecturas son frecuentes y las escrituras son raras, RWMutex es más eficiente:
type CachéRW struct {
mu sync.RWMutex
datos map[string]string
}
func (c *CachéRW) Set(clave, valor string) {
c.mu.Lock() // bloquea lectores Y escritores
defer c.mu.Unlock()
c.datos[clave] = valor
}
func (c *CachéRW) Get(clave string) (string, bool) {
c.mu.RLock() // permite lectores concurrentes, bloquea solo escritores
defer c.mu.RUnlock()
val, ok := c.datos[clave]
return val, ok
}`sync.Once`: inicialización única garantizada
Once garantiza que una función se ejecuta exactamente una vez, incluso si se llama desde múltiples goroutines:
type Servicio struct {
config *Config
}
var (
servicio *Servicio
once sync.Once
)
func ObtenerServicio() *Servicio {
once.Do(func() {
// Esta función se ejecuta exactamente una vez
servicio = &Servicio{
config: cargarConfigDesdeArchivo(),
}
})
return servicio
}`sync.Map`: map concurrente
Para maps con acceso concurrente frecuente, Go ofrece sync.Map:
var sm sync.Map
// Store, Load, LoadOrStore, Delete, Range
sm.Store("clave", "valor")
if val, ok := sm.Load("clave"); ok {
fmt.Println(val.(string))
}
sm.Range(func(k, v any) bool {
fmt.Printf("%v: %v\n", k, v)
return true // continuar iteración
})sync.Map está optimizado para dos casos: cuando el map es escrito una vez y leído muchas veces, o cuando múltiples goroutines leen y escriben claves disjuntas.
`sync/atomic`: operaciones atómicas de bajo nivel
Para contadores simples, atomic es más eficiente que un Mutex:
import "sync/atomic"
var contador int64 // debe ser int32, int64, uint32, uint64, o Pointer
// Operaciones atómicas garantizan atomicidad sin Mutex
atomic.AddInt64(&contador, 1)
atomic.AddInt64(&contador, -1)
val := atomic.LoadInt64(&contador)
atomic.StoreInt64(&contador, 0)
ok := atomic.CompareAndSwapInt64(&contador, 0, 1) // CASGoroutines y funciones anónimas: el patrón completo
func procesarConConcurrencia(tareas []Tarea) []Resultado {
resultados := make([]Resultado, len(tareas))
var wg sync.WaitGroup
var mu sync.Mutex
for i, tarea := range tareas {
wg.Add(1)
go func(idx int, t Tarea) {
defer wg.Done()
resultado, err := procesarTarea(t)
if err != nil {
log.Printf("error en tarea %d: %v", idx, err)
return
}
mu.Lock()
resultados[idx] = resultado
mu.Unlock()
}(i, tarea)
}
wg.Wait()
return resultados
}Con las goroutines dominadas, en la siguiente lección aprenderemos channels — el mecanismo de comunicación entre goroutines que hace que la concurrencia en Go sea segura y elegante.
Inicia sesión para guardar tu progreso