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.
