Lo schema di base
sync.WaitGroup conta le goroutine in esecuzione. Add aumenta il conteggio, Done lo diminuisce, Wait si blocca finché non arriva a zero.
I tre download girano in parallelo, quindi il programma impiega circa 10 ms invece di 30. I risultati vengono stampati nell'ordine dell'input perché ogni goroutine scrive solo il proprio indice di sizes, e main li legge solo dopo Wait.
Il valore zero di un WaitGroup è pronto all'uso. Nessun costruttore.
Le tre regole
Chiama Add prima di go, non dentro la goroutine. Se è la goroutine a chiamare Add, main può arrivare a Wait prima che qualsiasi goroutine sia partita, vedere un conteggio pari a zero e ritornare quando il lavoro non è nemmeno iniziato. Quando conosci il numero in anticipo, un solo wg.Add(len(files)) prima del ciclo è equivalente.
Chiama Done con defer come prima riga della goroutine. Una goroutine che ritorna in anticipo per un errore, o che va in panic, decrementa comunque il contatore. Un Done mancante lascia Wait bloccato per sempre. Se è l'unica goroutine rimasta, il runtime segnala fatal error: all goroutines are asleep con sync.WaitGroup.Wait nello stack trace.
Non copiare mai un WaitGroup dopo il primo utilizzo. Passa *sync.WaitGroup alle funzioni, oppure cattura la variabile in una closure come sopra.
Passare un WaitGroup a una funzione
Quando il corpo della goroutine è una funzione con nome, passa un puntatore:
Con wg sync.WaitGroup come parametro per valore, ogni worker chiamerebbe Done sulla propria copia e main resterebbe bloccato in Wait per sempre. go vet lo rileva prima che tu esegua qualsiasi cosa:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Un design più pulito tiene la concorrenza del tutto fuori da worker: lasciala una funzione normale e gestisci Add/Done nella closure del chiamante. Così worker è facile da testare e da chiamare in modo sincrono.
Contatore negativo
Done equivale ad Add(-1). Se il conteggio scende sotto zero, il programma va in panic:
L'output è recovered: sync: negative WaitGroup counter. La causa tipica è una goroutine con defer wg.Done() che in qualche ramo chiama anche wg.Done() esplicitamente.
Raccogliere gli errori
Un WaitGroup si limita a contare. Per gli errori, dai a ogni goroutine la sua posizione e controllale dopo Wait:
errors.Join (Go 1.20) salta i valori nil e restituisce nil se sono tutti nil, quindi combina "un errore per goroutine" senza alcuna gestione aggiuntiva.
Se vuoi fermare il lavoro rimanente appena una goroutine fallisce, usa invece golang.org/x/sync/errgroup. È un WaitGroup più il primo errore più un context che viene cancellato in caso di fallimento, e g.SetLimit(n) limita la concorrenza. Sta fuori dalla libreria standard, quindi non può girare nell'editor di questa pagina:
g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
return err // the first error; ctx was cancelled for the others
}
Un worker pool
Un numero fisso di goroutine che leggono job da un canale mantiene limitata la concorrenza, qualunque sia il numero di job. Il WaitGroup ti dice quando tutti i worker hanno finito, cioè quando si può chiudere il canale dei risultati.
L'ordine dei tre pezzi è importante:
maindeve ricevere i risultati mentre i worker girano. Semainchiamassewg.Wait()direttamente prima di leggere, i worker resterebbero bloccati nell'invio suresults, non arriverebbero mai aDonee tutto andrebbe in deadlock. Ecco perchéWaitgira in una goroutine a parte.close(results)avviene solo dopoWait, quindi nessun worker può inviare su un canale chiuso.- Anche chi fornisce i job gira in una goroutine, così l'invio e la raccolta si sovrappongono.
Quale worker gestisce quale job cambia da un'esecuzione all'altra, quindi il programma ordina per job prima di stampare. Tutto ciò che stampa è deterministico.
WaitGroup, canale o errgroup
| Esigenza | Usa |
|---|---|
| Aspettare N goroutine, risultati in posizioni indicizzate | sync.WaitGroup |
| Aspettare una sola goroutine | un canale done o il canale dei risultati stesso |
| Risultati trasmessi man mano che finiscono | un canale, chiuso dopo wg.Wait() |
| Fermare tutto al primo errore | errgroup.WithContext |
| Fermare tutto per un timeout o una cancellazione del chiamante | context.Context più un WaitGroup o un errgroup |
Go 1.25 aggiunge wg.Go(func() { ... }), che esegue per te Add(1) e il Done differito. Il codice per Go 1.24 e versioni precedenti, compreso l'editor di questa pagina, usa la forma esplicita mostrata sopra.
Errori comuni
wg.Add(1)dentro la goroutine.Waitpuò ritornare prima che venga eseguito.- Dimenticare
Donein un ritorno anticipato. Usa sempredefer wg.Done(). - Passare il WaitGroup per valore. Usa un puntatore;
go vetsegnala la copia. - Aspettare nella stessa goroutine che deve svuotare un canale. Sposta
wg.Wait()e ilclosein una goroutine separata. - Riusare un WaitGroup prima che il
Waitprecedente sia ritornato. Inizia un nuovo ciclo di chiamate adAddsolo dopo cheWaitha finito.
Domande frequenti
Come funziona sync.WaitGroup in Go?
Un WaitGroup è un contatore. wg.Add(n) lo incrementa, wg.Done() lo decrementa di uno e wg.Wait() si blocca finché non arriva a zero. Chiama Add prima di avviare ogni goroutine, defer wg.Done() al suo interno e Wait nel punto in cui ti serve che tutto sia finito.
Un WaitGroup va passato per valore o per puntatore?
Per puntatore (*sync.WaitGroup), oppure lascia che le goroutine lo catturino in una closure. Una copia ha il proprio contatore, quindi Done sulla copia non raggiunge mai l'originale e Wait resta bloccato per sempre. go vet segnala l'errore come "passes lock by value".
Cosa causa "sync: negative WaitGroup counter"?
Più chiamate a Done che ad Add. Di solito una goroutine chiama Done due volte (una con defer e una esplicitamente), oppure Add(1) viene saltato in un ramo. Il programma va in panic, perché il contatore non può più dirti nulla di vero.
Come ottengo gli errori dalle goroutine avviate con un WaitGroup?
Un WaitGroup non trasporta né risultati né errori. Dai a ogni goroutine la sua posizione in uno slice di errori e combinali dopo Wait (per esempio con errors.Join), oppure usa golang.org/x/sync/errgroup, il cui Wait restituisce il primo errore e può cancellare le altre goroutine tramite un context.