sync.WaitGroup en Go: esperar a que terminen todas las goroutines

sync.WaitGroup es la forma estándar en Go de esperar a que un conjunto de goroutines termine antes de continuar. Es más sencilla que gestionar canales de finalización cuando simplemente quieres que todo el trabajo acabe antes de seguir.

Tres métodos, un contador interno

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // incrementa el contador antes de lanzar la goroutine
        go func(n int) {
            defer wg.Done() // decrementa el contador al terminar
            fmt.Printf("goroutine %d terminadan", n)
        }(i)
    }

    wg.Wait() // bloquea hasta que el contador llega a 0
    fmt.Println("todas las goroutines han terminado")
}

La secuencia es siempre la misma: Add(1) antes de lanzar la goroutine, defer Done() como primera línea de la goroutine, y Wait() para bloquear al llamador.

El error del Add dentro de la goroutine

Este es el error más frecuente con WaitGroup. Si llamas a Add dentro de la goroutine, puede que Wait se ejecute antes de que la goroutine llegue a llamar a Add, y el programa continúa antes de tiempo:

// MAL: race condition, Wait puede ejecutarse antes del Add
for i := 0; i < 5; i++ {
    go func(n int) {
        wg.Add(1) // demasiado tarde
        defer wg.Done()
        trabajar(n)
    }(i)
}
wg.Wait() // puede retornar antes de que todas las goroutines arranquen

// BIEN: Add siempre antes de go
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        trabajar(n)
    }(i)
}

Patrón fan-out

Distribuir trabajo entre varias goroutines y esperar a que todo termine:

func procesarLote(elementos []string) {
    var wg sync.WaitGroup

    for _, elem := range elementos {
        wg.Add(1)
        go func(e string) {
            defer wg.Done()
            resultado := procesar(e)
            fmt.Println(resultado)
        }(elem)
    }

    wg.Wait()
    fmt.Println("lote completado")
}

Recoger resultados con mutex

Para recoger los resultados de las goroutines en una estructura compartida, combina WaitGroup con un mutex:

func buscarEnParalelo(consultas []string) []string {
    var (
        wg         sync.WaitGroup
        mu         sync.Mutex
        resultados []string
    )

    for _, q := range consultas {
        wg.Add(1)
        go func(query string) {
            defer wg.Done()
            r := buscar(query)
            mu.Lock()
            resultados = append(resultados, r)
            mu.Unlock()
        }(q)
    }

    wg.Wait()
    return resultados
}

errgroup: WaitGroup con manejo de errores

El paquete golang.org/x/sync/errgroup extiende WaitGroup para propagar errores desde las goroutines. Cancela automáticamente el contexto cuando alguna goroutine falla:

import "golang.org/x/sync/errgroup"

func obtenerDatos(urls []string) ([]string, error) {
    g, ctx := errgroup.WithContext(context.Background())
    resultados := make([]string, len(urls))

    for i, url := range urls {
        i, url := i, url // capturar variables
        g.Go(func() error {
            r, err := http.Get(url)
            if err != nil {
                return fmt.Errorf("GET %s: %w", url, err)
            }
            defer r.Body.Close()
            body, err := io.ReadAll(r.Body)
            if err != nil {
                return err
            }
            resultados[i] = string(body)
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    _ = ctx
    return resultados, nil
}

WaitGroup vs canales

Usa WaitGroup cuando solo necesitas saber cuándo terminan las goroutines y no necesitas pasar datos de vuelta. Usa canales cuando las goroutines producen resultados que el caller necesita procesar a medida que llegan.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP