Cuando varias goroutines acceden a datos compartidos y al menos una escribe, necesitas sincronización. El paquete sync de Go ofrece las herramientas fundamentales: Mutex para acceso exclusivo, RWMutex para permitir lecturas simultáneas, Once para inicialización única y sync/atomic para operaciones atómicas de bajo nivel.
sync.Mutex: acceso exclusivo
package main
import (
"fmt"
"sync"
)
type Contador struct {
mu sync.Mutex
valor int
}
func (c *Contador) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock() // siempre desbloquear, incluso si hay un pánico
c.valor++
}
func (c *Contador) Valor() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valor
}
func main() {
c := &Contador{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Incrementar()
}()
}
wg.Wait()
fmt.Println(c.Valor()) // siempre 1000
}
El defer c.mu.Unlock() después del Lock garantiza que el mutex se libera incluso si el código posterior entra en pánico.
Deadlock: el error más común con mutexes
Un deadlock ocurre cuando una goroutine espera un mutex que ella misma ya tiene bloqueado, o cuando dos goroutines esperan mutuamente al mutex de la otra. Go lo detecta y termina el programa:
// PELIGRO: Lock dentro de Lock sin Unlock intermedio
func (c *Contador) Doble() {
c.mu.Lock()
c.Incrementar() // intenta Lock de nuevo ? deadlock
c.mu.Unlock()
}
sync.RWMutex: lectores simultáneos
RWMutex permite que múltiples goroutines lean al mismo tiempo pero solo una escriba. Mejora el rendimiento en cargas de trabajo con muchas lecturas y pocas escrituras:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(clave string) (string, bool) {
c.mu.RLock() // bloqueo de lectura: otros también pueden leer
defer c.mu.RUnlock()
v, ok := c.data[clave]
return v, ok
}
func (c *Cache) Set(clave, valor string) {
c.mu.Lock() // bloqueo exclusivo: nadie más puede leer ni escribir
defer c.mu.Unlock()
c.data[clave] = valor
}
sync.Once: inicialización thread-safe
sync.Once garantiza que una función se ejecuta exactamente una vez, aunque múltiples goroutines la llamen simultáneamente. Es la forma idiomática de implementar el patrón singleton:
var (
instancia *BD
once sync.Once
)
func ObtenerBD() *BD {
once.Do(func() {
instancia = &BD{} // se ejecuta solo la primera vez
instancia.Conectar("localhost:5432")
})
return instancia
}
sync/atomic: operaciones atómicas
Para contadores simples, las operaciones atómicas son más eficientes que un mutex porque operan a nivel de instrucción de CPU sin bloquear:
import "sync/atomic"
type ContadorAtomico struct {
valor int64
}
func (c *ContadorAtomico) Incrementar() {
atomic.AddInt64(&c.valor, 1)
}
func (c *ContadorAtomico) Valor() int64 {
return atomic.LoadInt64(&c.valor)
}
// Desde Go 1.19: tipos atómicos genéricos
var bandera atomic.Bool
bandera.Store(true)
fmt.Println(bandera.Load()) // true
Cuándo usar cada mecanismo
sync.Mutex cuando varias goroutines necesitan acceso exclusivo a una estructura de datos compleja.sync.RWMutex cuando hay muchas más lecturas que escrituras (caches, registros).sync.Once para inicialización perezosa segura entre goroutines.sync/atomic para contadores simples y flags booleanos sin necesidad de proteger una estructura compleja.- Canales cuando necesitas comunicar datos entre goroutines, no solo sincronizar el acceso.
