sync.WaitGroup es un contador de sincronización para esperar a que varias goroutines terminen. Es la forma limpia de decir: no sigas hasta que todas hayan acabado.
En el artículo anterior usamos time.Sleep de forma provisional para esperar a que terminaran las goroutines.
Eso en un entorno real es inaceptable. Si pones un Sleep(5 segundos) y la tarea tarda 10ms, estás desperdiciando 4.99 segundos. Si la tarea tarda 6 segundos, el programa se cerrará antes de acabar y perderás datos.
Necesitamos una forma de decirle al programa principal: “No te cierres hasta que estas X goroutines hayan terminado”.
Para eso, la biblioteca estándar sync ofrece WaitGroup.
Qué es un WaitGroup
Un WaitGroup es, en esencia, un contador seguro para la concurrencia (Thread-safe counter).
Su funcionamiento es muy simple:
- Incrementas el contador cuando lanzas una tarea.
- Decrementas el contador cuando una tarea termina.
- Bloqueas la goroutine que llama a
Waithasta que el contador llegue a cero.
Los métodos clave
Para usarlo, necesitamos importar el paquete sync y conocer estos métodos:
Add(n int): Sumanal contador. Generalmente hacemosAdd(1)justo antes de lanzar la goroutine.Done(): Resta 1 al contador. Se llama dentro de la goroutine cuando ha terminado.Wait(): Bloquea la ejecución hasta que el contador interno sea 0.Go(f func()): Lanza una función en una goroutine y la añade al grupo automáticamente (Go 1.25+).
Ejemplo básico
Vamos a arreglar el ejemplo del artículo anterior.
package main
import (
"fmt"
"sync" // 1. Importamos sync
"time"
)
func trabajador(id int, wg *sync.WaitGroup) {
// 3. Al terminar la función, avisamos al WaitGroup
// Usamos defer para asegurar que se ejecuta al retornar
defer wg.Done()
fmt.Printf("Trabajador %d iniciando...\n", id)
time.Sleep(time.Second) // Simulamos trabajo
fmt.Printf("Trabajador %d completado\n", id)
}
func main() {
// Definimos el WaitGroup (su zero value ya es utilizable)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
// 2. Incrementamos el contador ANTES de lanzar la goroutine
wg.Add(1)
// Lanzamos la goroutine pasando el puntero del WaitGroup
go trabajador(i, &wg)
}
fmt.Println("El Main está esperando...")
// 4. Bloqueamos hasta que todas terminen (contador == 0)
wg.Wait()
fmt.Println("Todo el trabajo ha finalizado. Adiós.")
}En el patrón clásico, llama a wg.Add(1) antes de la sentencia go. Si lo haces dentro, wg.Wait() podría observar el contador a cero antes de que la goroutine haya arrancado.
El atajo moderno: WaitGroup.Go
Desde Go 1.25, sync.WaitGroup tiene el método Go, que agrupa el patrón Add(1), go func() y Done() en una sola llamada.
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
id := i
wg.Go(func() {
fmt.Printf("Trabajador %d iniciando...\n", id)
time.Sleep(time.Second)
fmt.Printf("Trabajador %d completado\n", id)
})
}
wg.Wait()
}Es más compacto y evita desajustes entre Add y Done. Su contrato exige que la función no provoque un panic; si una operación puede fallar de forma esperable, debemos gestionar o comunicar su error dentro de la tarea.
No copiar un WaitGroup
Fíjate bien en la firma de la función trabajador del ejemplo anterior:
func trabajador(id int, wg *sync.WaitGroup)
Estamos pasando un Puntero. ¿Por qué?
Un sync.WaitGroup es un Struct. En Go, todo se pasa por valor (copia).
Si pasas el WaitGroup sin puntero, la goroutine recibe una fotocopia del WaitGroup.
- La goroutine hace
Done()sobre su fotocopia. - El
mainespera sobre el WaitGroup original. - El original nunca llega a cero.
- Resultado: el original no llega a cero y
Waitpuede quedar bloqueado indefinidamente.
Nunca copies un WaitGroup. Si tienes que pasarlo a funciones, usa siempre un puntero *sync.WaitGroup.
Uso con funciones anónimas
Lo más habitual en Go es no crear una función externa como trabajador, sino lanzar funciones anónimas dentro del bucle. Aquí los Closures brillan.
func main() {
var wg sync.WaitGroup
urls := []string{"http://google.com", "http://spotify.com", "http://luisllamas.es"}
for _, url := range urls {
wg.Add(1)
// Lanzamos función anónima
go func(u string) {
defer wg.Done()
// Simulamos petición
fmt.Printf("Visitando %s\n", u)
}(url) // Pasamos 'url' como argumento
}
wg.Wait()
}El closure captura la variable wg, así que trabaja con la misma instancia del ámbito de main. No necesitamos pasarla como parámetro.
WaitGroup frente a canales
Es importante entender cuándo usar qué.
- Usa WaitGroups cuando solo te importa esperar a que terminen un conjunto de tareas, pero no necesitas que te devuelvan resultados.
- Usa Canales (que veremos a continuación) cuando necesitas que las goroutines se comuniquen o te devuelvan datos.