← Volver al listado de tecnologías

Sync y Context

Por: Artiko
gosynccontextmutexwaitgroup

Sync y Context

Go provee el paquete sync para coordinar goroutines mediante primitivas de sincronizacion, y el paquete context para controlar cancelacion, deadlines y propagacion de valores.

sync.WaitGroup

Espera a que un conjunto de goroutines termine su trabajo.

package main

import (
    "fmt"
    "sync"
)

func procesar(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Decrementa el contador al terminar
    fmt.Printf("Worker %d procesando\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // Incrementa antes de lanzar la goroutine
        go procesar(i, &wg)
    }

    wg.Wait() // Bloquea hasta que el contador llegue a 0
    fmt.Println("Todos los workers terminaron")
}
MetodoDescripcion
Add(n)Incrementa el contador en n
Done()Decrementa el contador en 1 (equivale a Add(-1))
Wait()Bloquea hasta que el contador sea 0

Regla clave: Siempre llama Add() antes de lanzar la goroutine, nunca dentro.

sync.Mutex

Protege secciones criticas donde multiples goroutines acceden a datos compartidos.

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) Obtener() 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("Valor final:", c.Obtener()) // 1000
}

sync.RWMutex

Permite multiples lectores simultaneos pero solo un escritor exclusivo.

package main

import (
    "fmt"
    "sync"
)

type Cache struct {
    mu    sync.RWMutex
    datos map[string]string
}

func (c *Cache) Leer(clave string) (string, bool) {
    c.mu.RLock()         // Permite multiples lectores
    defer c.mu.RUnlock()
    val, ok := c.datos[clave]
    return val, ok
}

func (c *Cache) Escribir(clave, valor string) {
    c.mu.Lock()          // Exclusivo para escritura
    defer c.mu.Unlock()
    c.datos[clave] = valor
}

func main() {
    cache := &Cache{datos: make(map[string]string)}
    cache.Escribir("nombre", "Go")

    val, _ := cache.Leer("nombre")
    fmt.Println(val) // Go
}
TipoLectores simultaneosEscritores simultaneosUso ideal
MutexNoNoEscritura frecuente
RWMutexSiNoLectura frecuente, escritura rara

sync.Once

Garantiza que una funcion se ejecute exactamente una vez, sin importar cuantas goroutines la invoquen.

package main

import (
    "fmt"
    "sync"
)

var (
    instancia *Config
    once      sync.Once
)

type Config struct {
    Puerto int
}

func ObtenerConfig() *Config {
    once.Do(func() {
        fmt.Println("Inicializando config (solo una vez)")
        instancia = &Config{Puerto: 8080}
    })
    return instancia
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            cfg := ObtenerConfig()
            fmt.Println("Puerto:", cfg.Puerto)
        }()
    }
    wg.Wait()
}

sync.Map

Mapa concurrente optimizado para casos donde las claves se escriben una vez y se leen muchas.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Escribir
    m.Store("go", 2009)
    m.Store("rust", 2015)

    // Leer
    val, ok := m.Load("go")
    fmt.Println(val, ok) // 2009 true

    // Leer o escribir si no existe
    actual, loaded := m.LoadOrStore("zig", 2016)
    fmt.Println(actual, loaded) // 2016 false

    // Iterar
    m.Range(func(key, value any) bool {
        fmt.Printf("%s: %v\n", key, value)
        return true // continuar iterando
    })

    // Eliminar
    m.Delete("rust")
}

Nota: Prefiere map + sync.Mutex para la mayoria de casos. sync.Map es mejor cuando las claves son estables y hay muchos lectores concurrentes.

Package context

El paquete context permite propagar senales de cancelacion, deadlines y valores a traves del arbol de llamadas.

context.Background y context.TODO

// Raiz de todo arbol de contextos, nunca se cancela
ctx := context.Background()

// Placeholder cuando no sabes que contexto usar aun
ctx := context.TODO()

context.WithCancel

Cancelacion manual desde el padre.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: cancelado (%v)\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d: trabajando\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(2 * time.Second)
    cancel() // Cancela todos los workers
    time.Sleep(100 * time.Millisecond)
}

context.WithTimeout

Cancelacion automatica despues de una duracion.

package main

import (
    "context"
    "fmt"
    "time"
)

func operacionLenta(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("Operacion completada")
        return nil
    case <-ctx.Done():
        return ctx.Err() // context.DeadlineExceeded
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel() // Siempre llamar cancel para liberar recursos

    if err := operacionLenta(ctx); err != nil {
        fmt.Println("Error:", err) // context deadline exceeded
    }
}

context.WithDeadline

Similar a WithTimeout pero con un momento absoluto.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    deadline := time.Now().Add(3 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    dl, ok := ctx.Deadline()
    fmt.Println("Deadline:", dl, "Tiene deadline:", ok)

    <-ctx.Done()
    fmt.Println("Contexto expirado:", ctx.Err())
}

context.WithValue

Propaga valores a traves de la cadena de llamadas. Usar con moderacion.

package main

import (
    "context"
    "fmt"
)

// Tipo privado para claves de contexto (evita colisiones)
type ctxKey string

const requestIDKey ctxKey = "requestID"

func middleware(ctx context.Context, reqID string) context.Context {
    return context.WithValue(ctx, requestIDKey, reqID)
}

func handler(ctx context.Context) {
    reqID, ok := ctx.Value(requestIDKey).(string)
    if !ok {
        reqID = "desconocido"
    }
    fmt.Println("Request ID:", reqID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx, "abc-123")
    handler(ctx)
}

Propagacion de contexto

El contexto fluye del padre a los hijos formando un arbol:

package main

import (
    "context"
    "fmt"
    "time"
)

func consultarDB(ctx context.Context) error {
    select {
    case <-time.After(1 * time.Second):
        return nil
    case <-ctx.Done():
        return fmt.Errorf("db: %w", ctx.Err())
    }
}

func consultarCache(ctx context.Context) error {
    select {
    case <-time.After(200 * time.Millisecond):
        return nil
    case <-ctx.Done():
        return fmt.Errorf("cache: %w", ctx.Err())
    }
}

func procesarRequest(ctx context.Context) error {
    // Ambas operaciones heredan el timeout del padre
    if err := consultarCache(ctx); err != nil {
        return err
    }
    return consultarDB(ctx)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    if err := procesarRequest(ctx); err != nil {
        fmt.Println("Error:", err)
    }
}

Buenas practicas

PracticaDescripcion
Context como primer parametrofunc Hacer(ctx context.Context, ...)
Siempre llamar cancel()Usa defer cancel() inmediatamente
No guardar context en structsPasalo como parametro de funcion
Claves tipadas para WithValueUsa tipos privados para evitar colisiones
Preferir cancelacion sobre timeoutMas control explícito
Verificar ctx.Err()Antes de operaciones costosas
// Correcto: context como primer parametro
func ObtenerUsuario(ctx context.Context, id int) (*Usuario, error) {
    // Verificar cancelacion antes de query costoso
    if ctx.Err() != nil {
        return nil, ctx.Err()
    }
    // ... hacer query
    return nil, nil
}

← Cap 16: Channels y Select | Cap 18: Testing en Go →