Sync y Context
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")
}
| Metodo | Descripcion |
|---|---|
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
}
| Tipo | Lectores simultaneos | Escritores simultaneos | Uso ideal |
|---|---|---|---|
Mutex | No | No | Escritura frecuente |
RWMutex | Si | No | Lectura 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.Mutexpara la mayoria de casos.sync.Mapes 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
| Practica | Descripcion |
|---|---|
| Context como primer parametro | func Hacer(ctx context.Context, ...) |
Siempre llamar cancel() | Usa defer cancel() inmediatamente |
| No guardar context en structs | Pasalo como parametro de funcion |
| Claves tipadas para WithValue | Usa tipos privados para evitar colisiones |
| Preferir cancelacion sobre timeout | Mas 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
}