# Sincronización en Go: Mutex, RWMutex y WaitGroup

En los posts anteriores exploramos las **goroutines**, **channels**, **select** y **context**. Hoy vamos a descubrir las primitivas de sincronización de bajo nivel del paquete `sync`. Si vienes de Java, estas son similares a `synchronized`, `ReentrantLock`, `ReadWriteLock` y `CountDownLatch`, pero con una filosofía diferente.

## **¿Por Qué Necesitamos Sincronización?**

Cuando múltiples goroutines acceden a datos compartidos sin sincronización, pueden ocurrir **race conditions** (condiciones de carrera). Esto puede causar:

* ❌ Valores incorrectos
    
* ❌ Pérdida de datos
    
* ❌ Comportamiento impredecible
    
* ❌ Crashes en producción
    

### **Comparación: Java vs Go**

| Aspecto | Java | Go |
| --- | --- | --- |
| Exclusión mutua | `synchronized`, `ReentrantLock` | `sync.Mutex` |
| Read-Write Lock | `ReadWriteLock` | `sync.RWMutex` |
| Esperar goroutines | `CountDownLatch`, `join()` | `sync.WaitGroup` |
| Inicialización única | `volatile` + double-check | `sync.Once` |
| Condition variables | `Condition` | `sync.Cond` |
| Operaciones atómicas | `AtomicInteger`, etc. | `sync/atomic` |

## **sync.Mutex: Exclusión Mutua**

`Mutex` (mutual exclusion) protege secciones críticas del código. Solo una goroutine puede tener el lock a la vez.

### **Ejemplo Básico: Contador Thread-Safe**

```go
var mu sync.Mutex
counter := 0

// Incrementar contador de forma segura
increment := func() {
    mu.Lock()
    defer mu.Unlock() // ← Siempre usar defer para unlock
    counter++
}

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        increment()
    }()
}

wg.Wait()
fmt.Printf("Counter: %d (should be 1000)\n", counter)
```

**Características importantes:**

* ✅ `Lock()` bloquea hasta que el mutex esté disponible
    
* ✅ `Unlock()` libera el mutex
    
* ✅ **Siempre usa** `defer mu.Unlock()` para asegurar que se libere incluso si hay panic
    
* ✅ Solo una goroutine puede tener el lock a la vez
    

### **❌ Sin Mutex: Data Race**

```go
// ❌ MAL: Data race
var counter int

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++ // ← Múltiples goroutines escriben sin lock
    }()
}

wg.Wait()
fmt.Printf("Counter: %d (may not be 10 due to race condition)\n", counter)
```

**Ejecutar con race detector:**

```bash
go run -race main.go
```

El race detector detectará el problema y mostrará warnings.

### **Mejores Prácticas con Mutex**

**1\. Siempre usa defer para Unlock:**

```go
// ✅ BIEN
mu.Lock()
defer mu.Unlock()
// Código que puede hacer panic
riskyOperation()

// ❌ MAL: Puede no liberar el lock si hay panic
mu.Lock()
riskyOperation() // Si hace panic, el lock nunca se libera
mu.Unlock()
```

**2\. Mantén las secciones críticas pequeñas:**

```go
// ✅ BIEN: Sección crítica pequeña
mu.Lock()
value := sharedData[key]
mu.Unlock()
processValue(value) // Procesar fuera del lock

// ❌ MAL: Sección crítica grande
mu.Lock()
value := sharedData[key]
processValue(value) // Bloquea el lock durante procesamiento largo
mu.Unlock()
```

**3\. No copies un Mutex:**

```go
// ❌ MAL: Copiar mutex
type Counter struct {
    mu sync.Mutex
    count int
}

func (c Counter) Increment() { // ← Recibe por valor, copia el mutex
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// ✅ BIEN: Usar pointer receiver
func (c *Counter) Increment() { // ← Recibe por referencia
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}
```

## **sync.RWMutex: Read-Write Mutex**

`RWMutex` permite **múltiples lectores simultáneos** o **un escritor exclusivo**. Es más eficiente que `Mutex` cuando tienes muchas lecturas y pocas escrituras.

### **Ejemplo: Map Thread-Safe**

```go
type SafeMap struct {
    mu   sync.RWMutex
    data map[string]int
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        data: make(map[string]int),
    }
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()         // Lock para lectura (múltiples lectores permitidos)
    defer sm.mu.RUnlock() // Unlock para lectura
    value, exists := sm.data[key]
    return value, exists
}

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()         // Lock para escritura (exclusivo)
    defer sm.mu.Unlock() // Unlock para escritura
    sm.data[key] = value
}
```

**Características:**

* ✅ `RLock()`: Lock para lectura (múltiples goroutines pueden leer simultáneamente)
    
* ✅ `RUnlock()`: Unlock para lectura
    
* ✅ `Lock()`: Lock para escritura (exclusivo, bloquea lectores y escritores)
    
* ✅ `Unlock()`: Unlock para escritura
    

### **Cuándo Usar RWMutex vs Mutex**

```go
// ✅ Usa RWMutex cuando:
// - Tienes muchas lecturas y pocas escrituras
// - Las lecturas son independientes
// - Quieres mejor rendimiento en lectura

// ✅ Usa Mutex cuando:
// - Tienes escrituras frecuentes
// - Las operaciones son simples
// - No necesitas optimización de lectura
```

### **Ejemplo: Múltiples Lectores y Escritores**

```go
sm := NewSafeMap()

// Escritores
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        sm.Set(fmt.Sprintf("key%d", id), id)
    }(i)
}

// Lectores (pueden leer simultáneamente)
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        value, exists := sm.Get(fmt.Sprintf("key%d", id))
        if exists {
            fmt.Printf("Read key%d: %d\n", id, value)
        }
    }(i)
}

wg.Wait()
```

**Ventaja de rendimiento:**

* Con `Mutex`: Solo una goroutine puede leer a la vez
    
* Con `RWMutex`: Múltiples goroutines pueden leer simultáneamente
    

## **sync.WaitGroup: Esperar Múltiples Goroutines**

`WaitGroup` permite esperar a que múltiples goroutines terminen. Es similar a `CountDownLatch` en Java.

### **Ejemplo Básico**

```go
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1) // Incrementar contador
    go func(id int) {
        defer wg.Done() // Decrementar contador cuando termine
        fmt.Printf("Worker %d: Starting\n", id)
        time.Sleep(time.Duration(id) * 100 * time.Millisecond)
        fmt.Printf("Worker %d: Finished\n", id)
    }(i)
}

fmt.Println("Waiting for all workers...")
wg.Wait() // Bloquea hasta que el contador llegue a 0
fmt.Println("All workers finished!")
```

**Cómo funciona:**

1. `wg.Add(n)`: Incrementa el contador interno en `n`
    
2. `wg.Done()`: Decrementa el contador en 1 (equivalente a `Add(-1)`)
    
3. `wg.Wait()`: Bloquea hasta que el contador llegue a 0
    

### **Patrón Recomendado**

```go
// ✅ BIEN: Siempre usar defer wg.Done()
wg.Add(1)
go func() {
    defer wg.Done() // ← Siempre se ejecuta, incluso si hay panic
    riskyOperation()
}()

// ❌ MAL: Puede no ejecutarse si hay panic
wg.Add(1)
go func() {
    riskyOperation() // Si hace panic, Done() nunca se llama
    wg.Done()
}()
```

### **WaitGroup con Contador Dinámico**

```go
var wg sync.WaitGroup

// Agregar trabajos dinámicamente
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        
        // Este worker puede crear más trabajos
        if id%2 == 0 {
            wg.Add(1)
            go func(subID int) {
                defer wg.Done()
                fmt.Printf("Sub-worker %d-%d\n", id, subID)
            }(id * 10)
        }
    }(i)
}

wg.Wait()
```

**⚠️ Importante:** `Add()` debe llamarse **antes** de lanzar la goroutine, no dentro de ella.

## **sync.Once: Ejecutar Una Vez**

`Once` garantiza que una función se ejecute solo una vez, incluso si múltiples goroutines la llaman. Es útil para inicialización lazy y singletons.

### **Ejemplo: Inicialización Lazy**

```go
var once sync.Once
var instance *Database

func getDatabase() *Database {
    once.Do(func() {
        fmt.Println("Initializing database (should only see this once)")
        instance = &Database{
            // Inicialización costosa
        }
    })
    return instance
}

// Múltiples goroutines pueden llamar getDatabase()
// Pero la inicialización solo ocurre una vez
for i := 0; i < 5; i++ {
    go func() {
        db := getDatabase()
        fmt.Printf("Got database: %p\n", db)
    }()
}
```

**Características:**

* ✅ Thread-safe
    
* ✅ Garantiza ejecución única
    
* ✅ Más eficiente que usar Mutex para inicialización
    

### **Comparación: Once vs Mutex**

```go
// ❌ Con Mutex (más verboso)
var mu sync.Mutex
var initialized bool
var instance *Database

func getDatabase() *Database {
    mu.Lock()
    defer mu.Unlock()
    if !initialized {
        instance = &Database{}
        initialized = true
    }
    return instance
}

// ✅ Con Once (más simple)
var once sync.Once
var instance *Database

func getDatabase() *Database {
    once.Do(func() {
        instance = &Database{}
    })
    return instance
}
```

## **sync.Cond: Condition Variables**

`Cond` permite que goroutines esperen por condiciones específicas. Es útil cuando necesitas esperar por un estado particular.

### **Ejemplo: Worker que Espera Condición**

```go
var mu sync.Mutex
cond := sync.NewCond(&mu)
ready := false

// Worker que espera condición
go func() {
    mu.Lock()
    for !ready {
        fmt.Println("Worker: Waiting for condition...")
        cond.Wait() // Libera el lock y espera
    }
    fmt.Println("Worker: Condition met, proceeding!")
    mu.Unlock()
}()

// Simular trabajo antes de señalizar
time.Sleep(1 * time.Second)

// Señalizar condición
mu.Lock()
ready = true
fmt.Println("Main: Signaling condition...")
cond.Signal() // Despierta una goroutine esperando
mu.Unlock()
```

**Métodos importantes:**

* `Wait()`: Libera el lock y espera. Debe llamarse con el lock adquirido.
    
* `Signal()`: Despierta una goroutine esperando
    
* `Broadcast()`: Despierta todas las goroutines esperando
    

**⚠️ Importante:** `Wait()` debe llamarse con el lock adquirido y dentro de un loop que verifica la condición:

```go
// ✅ BIEN: Loop con verificación
mu.Lock()
for !condition {
    cond.Wait()
}
// Condición es verdadera aquí
mu.Unlock()

// ❌ MAL: Sin loop (puede tener spurious wakeups)
mu.Lock()
if !condition {
    cond.Wait() // Puede despertar sin que la condición sea verdadera
}
mu.Unlock()
```

## **sync.Pool: Object Pooling**

`Pool` mantiene un conjunto de objetos reutilizables para reducir allocations. Es útil cuando crear objetos es costoso.

### **Ejemplo: Pool de Buffers**

```go
var pool = sync.Pool{
    New: func() interface{} {
        fmt.Println("Creating new buffer")
        return make([]byte, 1024)
    },
}

// Obtener del pool
buf1 := pool.Get().([]byte)
fmt.Printf("Got buffer from pool: len=%d\n", len(buf1))

// Devolver al pool
pool.Put(buf1)

// Obtener de nuevo (puede ser el mismo objeto)
buf2 := pool.Get().([]byte)
fmt.Printf("Got buffer from pool again: len=%d\n", len(buf2))

pool.Put(buf2)
```

**Características:**

* ✅ Reduce allocations
    
* ✅ Los objetos pueden ser recolectados por GC
    
* ✅ Thread-safe
    
* ⚠️ No garantiza que obtengas el mismo objeto
    

**Cuándo usar Pool:**

* Crear objetos es costoso
    
* Los objetos se reutilizan frecuentemente
    
* Quieres reducir presión en el GC
    

## **Operaciones Atómicas: sync/atomic**

Para operaciones simples en tipos primitivos, las operaciones atómicas son más eficientes que Mutex.

### **Ejemplo: Contador Atómico**

```go
var counter int64

// Incrementar atómicamente
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        atomic.AddInt64(&counter, 1)
    }()
}

wg.Wait()
fmt.Printf("Counter: %d (should be 1000)\n", atomic.LoadInt64(&counter))

// Compare and swap
oldValue := atomic.LoadInt64(&counter)
newValue := oldValue + 100
if atomic.CompareAndSwapInt64(&counter, oldValue, newValue) {
    fmt.Printf("CAS succeeded: %d -> %d\n", oldValue, newValue)
}
```

**Operaciones atómicas disponibles:**

* `AddInt64`, `AddInt32`, `AddUint64`, etc.
    
* `LoadInt64`, `LoadInt32`, etc.
    
* `StoreInt64`, `StoreInt32`, etc.
    
* `CompareAndSwapInt64`, `CompareAndSwapInt32`, etc.
    
* `SwapInt64`, `SwapInt32`, etc.
    

**Cuándo usar atomic vs Mutex:**

* ✅ **Atomic**: Operaciones simples en tipos primitivos (contadores, flags)
    
* ✅ **Mutex**: Operaciones complejas, múltiples variables, estructuras
    

## **Ejemplo Práctico: Cache Thread-Safe**

Combinando RWMutex y atomic operations:

```go
type Cache struct {
    mu    sync.RWMutex
    data  map[string]interface{}
    stats struct {
        hits   int64
        misses int64
    }
}

func NewCache() *Cache {
    return &Cache{
        data: make(map[string]interface{}),
    }
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    value, exists := c.data[key]
    c.mu.RUnlock()

    if exists {
        atomic.AddInt64(&c.stats.hits, 1)
    } else {
        atomic.AddInt64(&c.stats.misses, 1)
    }

    return value, exists
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache) Stats() (hits, misses int64) {
    return atomic.LoadInt64(&c.stats.hits), atomic.LoadInt64(&c.stats.misses)
}
```

**Características del diseño:**

* ✅ `RWMutex` para el map (muchas lecturas, pocas escrituras)
    
* ✅ Operaciones atómicas para estadísticas (más eficiente que Mutex)
    
* ✅ Sección crítica pequeña en `Get()` (solo para leer el map)
    

## **Detectar Data Races**

Go tiene un **race detector** incorporado que detecta race conditions en tiempo de ejecución.

### **Ejecutar con Race Detector**

```bash
# Ejecutar programa
go run -race main.go

# Ejecutar tests
go test -race

# Compilar binario
go build -race
```

### **Ejemplo de Data Race Detectado**

```go
// ❌ Código con data race
var counter int

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++ // ← Data race aquí
    }()
}

wg.Wait()
```

**Ejecutar con** `-race`:

```go
WARNING: DATA RACE
Read at 0x00c00001a0a8 by goroutine 8:
  main.main.func1()
      /path/to/main.go:361: +0x44

Previous write at 0x00c00001a0a8 by goroutine 7:
  main.main.func1()
      /path/to/main.go:361: +0x60
```

## **Mejores Prácticas**

### **1\. Prefiere Channels sobre Mutex cuando sea Posible**

```go
// ✅ BIEN: Usar channels (más idiomático en Go)
ch := make(chan int, 1)
ch <- value
value := <-ch

// ⚠️ Usar Mutex solo cuando channels no son apropiados
var mu sync.Mutex
mu.Lock()
sharedValue = newValue
mu.Unlock()
```

**Regla general:** "Don't communicate by sharing memory; share memory by communicating"

### **2\. Siempre Usa defer para Unlock**

```go
// ✅ BIEN
mu.Lock()
defer mu.Unlock()
riskyOperation()

// ❌ MAL
mu.Lock()
riskyOperation() // Si hace panic, el lock nunca se libera
mu.Unlock()
```

### **3\. Mantén Secciones Críticas Pequeñas**

```go
// ✅ BIEN: Sección crítica pequeña
mu.Lock()
data := sharedMap[key]
mu.Unlock()
processData(data) // Procesar fuera del lock

// ❌ MAL: Sección crítica grande
mu.Lock()
data := sharedMap[key]
processData(data) // Bloquea el lock durante procesamiento
mu.Unlock()
```

### **4\. No Copies Mutexes**

```go
// ❌ MAL: Copiar mutex
func (c Counter) Increment() { // ← Recibe por valor
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// ✅ BIEN: Usar pointer receiver
func (c *Counter) Increment() { // ← Recibe por referencia
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}
```

### **5\. Usa RWMutex para Read-Heavy Workloads**

```go
// ✅ BIEN: Muchas lecturas, pocas escrituras
type Cache struct {
    mu sync.RWMutex
    data map[string]interface{}
}

func (c *Cache) Get(key string) interface{} {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}
```

### **6\. Siempre Usa defer con WaitGroup.Done()**

```go
// ✅ BIEN
wg.Add(1)
go func() {
    defer wg.Done()
    riskyOperation()
}()

// ❌ MAL
wg.Add(1)
go func() {
    riskyOperation()
    wg.Done() // Puede no ejecutarse si hay panic
}()
```

## **Errores Comunes**

### **❌ Error 1: Olvidar Unlock**

```go
// ❌ MAL: Deadlock
mu.Lock()
// Olvidamos Unlock()
// Todas las demás goroutines bloquean para siempre

// ✅ BIEN: Siempre usar defer
mu.Lock()
defer mu.Unlock()
```

### **❌ Error 2: Copiar Mutex**

```go
// ❌ MAL: Copiar mutex no funciona
type Counter struct {
    mu sync.Mutex
    count int
}

func (c Counter) Increment() { // ← Copia el mutex
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++ // Solo modifica la copia
}

// ✅ BIEN: Usar pointer receiver
func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}
```

### **❌ Error 3: Llamar Add() Dentro de la Goroutine**

```go
// ❌ MAL: Add() dentro de la goroutine puede causar race
go func() {
    wg.Add(1) // ← Puede ejecutarse después de Wait()
    defer wg.Done()
    // ...
}()

// ✅ BIEN: Add() antes de lanzar la goroutine
wg.Add(1)
go func() {
    defer wg.Done()
    // ...
}()
```

### **❌ Error 4: No Verificar Condición en Loop con Cond**

```go
// ❌ MAL: Puede tener spurious wakeups
mu.Lock()
if !ready {
    cond.Wait() // Puede despertar sin que ready sea true
}
mu.Unlock()

// ✅ BIEN: Verificar en loop
mu.Lock()
for !ready {
    cond.Wait()
}
mu.Unlock()
```

### **❌ Error 5: Usar Mutex para Operaciones Simples**

```go
// ❌ MAL: Mutex para operación simple
var mu sync.Mutex
var counter int64

func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

// ✅ BIEN: Usar atomic
var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}
```

## **Comparación: Java vs Go**

### **Java: synchronized y Locks**

```java
// Java
public class Counter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}

// O con ReentrantLock
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}
```

**Problemas:**

* Sintaxis verbosa
    
* Fácil olvidar unlock en finally
    
* No hay defer equivalente
    

### **Go: Mutex**

```go
// Go
type Counter struct {
    mu sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}
```

**Ventajas:**

* Sintaxis simple
    
* `defer` garantiza unlock
    
* Más idiomático
    

## **Conclusiones**

Las primitivas de sincronización en Go son poderosas y simples:

✅ **Mutex**: Exclusión mutua simple y efectiva

✅ **RWMutex**: Optimizado para read-heavy workloads

✅ **WaitGroup**: Esperar múltiples goroutines de forma elegante

✅ **Once**: Inicialización thread-safe garantizada

✅ **Cond**: Esperar por condiciones específicas

✅ **Pool**: Reducir allocations con object pooling

✅ **Atomic**: Operaciones atómicas eficientes para tipos primitivos

✅ **Race Detector**: Herramienta incorporada para detectar race conditions

Si vienes de Java, verás que Go simplifica mucho la sincronización. La filosofía de Go es usar channels cuando sea posible, y solo usar Mutex cuando sea absolutamente necesario. Sin embargo, cuando necesitas sincronización de bajo nivel, el paquete `sync` proporciona todas las herramientas necesarias.

## **Próximos Pasos**

En el siguiente post exploraremos **patrones avanzados de concurrencia** en Go, incluyendo worker pools, pipelines, fan-out/fan-in, y otros patrones comunes que combinan goroutines, channels, context y sync.

---

**¿Has usado primitivas de sincronización en tus proyectos de Go?** ¿Prefieres channels o Mutex? Comparte tus experiencias y casos de uso en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio [go-mastery-lab](https://github.com/joedayz/go-mastery-lab).
