Cuando varias goroutines acceden a datos compartidos y al menos una escribe, necesitas sincronización explícita. El paquete sync de Go ofrece las primitivas fundamentales: Mutex para acceso exclusivo, RWMutex para lectura concurrente y escritura exclusiva, WaitGroup para esperar goroutines, Once para inicialización única, Pool para reutilizar objetos y sync.Map para mapas concurrentes.
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()
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
}
Nunca copies un Mutex por valor: las goroutines que usen la copia tendrán un mutex distinto y la sincronización no funcionará. Pasa siempre un puntero a la estructura que lo contiene.
sync.RWMutex: lecturas concurrentes
RWMutex permite que múltiples lectores accedan simultáneamente, pero la escritura es exclusiva. Mejora el rendimiento en caches y registros donde las lecturas dominan:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(clave string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[clave]
return v, ok
}
func (c *Cache) Set(clave, valor string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[clave] = valor
}
sync.WaitGroup: esperar a un grupo de goroutines
WaitGroup lleva la cuenta de goroutines activas. Add(n) incrementa el contador, Done() lo decrementa y Wait() bloquea hasta llegar a cero:
func procesarLote(urls []string) {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fmt.Println("descargando:", u)
}(url)
}
wg.Wait()
fmt.Println("todas las descargas completadas")
}
sync.Once: inicialización garantizada
Once.Do ejecuta la función exactamente una vez aunque la llamen múltiples goroutines en paralelo. Ideal para el patrón singleton:
var (
instanciaBD *BD
once sync.Once
)
func ObtenerBD() *BD {
once.Do(func() {
instanciaBD = &BD{}
instanciaBD.Conectar("localhost:5432")
})
return instanciaBD
}
sync.Pool: reutilizar objetos costosos
Pool mantiene un conjunto de objetos reutilizables para reducir la presión sobre el GC. Muy útil para buffers y estructuras de uso frecuente:
var bufPool = sync.Pool{
New: func() any {
return make([]byte, 4096)
},
}
func handler(datos []byte) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf)
// usar buf...
copy(buf, datos)
fmt.Printf("procesados %d bytesn", len(datos))
}
El GC puede vaciar el pool en cualquier momento, así que úsalo solo para mejorar el rendimiento, no para garantizar persistencia de objetos.
sync.Map: mapa concurrente
sync.Map es un mapa seguro para acceso concurrente sin necesidad de un mutex externo. Está optimizado para patrones de escritura-una-vez y lectura-frecuente:
var m sync.Map
// Store y Load
m.Store("clave", "valor")
if v, ok := m.Load("clave"); ok {
fmt.Println(v.(string))
}
// LoadOrStore: carga si existe, guarda si no
actual, loaded := m.LoadOrStore("clave", "nuevo")
fmt.Println(actual, loaded) // "valor", true
// Range: iterar sobre todos los pares
m.Range(func(k, v any) bool {
fmt.Printf("%s ? %sn", k, v)
return true // devuelve false para parar la iteración
})
Para la mayoría de casos, un map normal con sync.RWMutex es más eficiente y más fácil de razonar que sync.Map. Usa sync.Map cuando tengas muchas goroutines leyendo claves distintas que raramente se solapan.
