Le motif de base
sync.WaitGroup compte les goroutines en cours. Add augmente le compte, Done le diminue, Wait bloque jusqu'à ce qu'il vaille zéro.
Les trois téléchargements s'exécutent en concurrence, donc le programme prend environ 10 ms au lieu de 30. Les résultats s'affichent dans l'ordre d'entrée parce que chaque goroutine n'écrit que son propre indice de sizes, et que main ne les lit qu'après Wait.
La valeur zéro d'un WaitGroup est prête à l'emploi. Pas de constructeur.
Les trois règles
Appelez Add avant go, pas dans la goroutine. Si la goroutine appelle Add elle-même, main peut atteindre Wait avant qu'aucune goroutine n'ait démarré, voir un compte à zéro, et retourner alors que le travail n'a pas commencé. Quand vous connaissez le compte à l'avance, un seul wg.Add(len(files)) avant la boucle est équivalent.
Appelez Done avec defer en première ligne de la goroutine. Une goroutine qui retourne tôt sur une erreur, ou qui provoque un panic, décrémente quand même le compteur. Un Done manquant laisse Wait bloqué indéfiniment. Si c'est la seule goroutine restante, le runtime signale fatal error: all goroutines are asleep avec sync.WaitGroup.Wait dans la trace.
Ne copiez jamais un WaitGroup après sa première utilisation. Passez *sync.WaitGroup aux fonctions, ou capturez la variable dans une closure comme ci-dessus.
Passer un WaitGroup à une fonction
Quand le corps de la goroutine est une fonction nommée, passez un pointeur :
Avec wg sync.WaitGroup comme paramètre valeur, chaque worker appellerait Done sur sa propre copie et main bloquerait indéfiniment dans Wait. go vet le détecte avant toute exécution :
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Une conception plus propre garde la concurrence entièrement hors de worker : laissez-la être une fonction ordinaire et faites la comptabilité Add/Done dans la closure de l'appelant. worker est alors facile à tester et à appeler de façon synchrone.
Compteur négatif
Done équivaut à Add(-1). Si le compte passe sous zéro, le programme provoque un panic :
La sortie est recovered: sync: negative WaitGroup counter. La cause habituelle est une goroutine avec defer wg.Done() qui appelle aussi wg.Done() explicitement sur un chemin.
Collecter les erreurs
Un WaitGroup ne fait que compter. Pour les erreurs, donnez à chaque goroutine sa propre case et examinez-les après Wait :
errors.Join (Go 1.20) ignore les valeurs nil et renvoie nil si toutes valent nil, donc il combine « une erreur par goroutine » sans comptabilité supplémentaire.
Si vous voulez arrêter le travail restant dès qu'une goroutine échoue, utilisez plutôt golang.org/x/sync/errgroup. C'est un WaitGroup plus la première erreur plus un context annulé en cas d'échec, et g.SetLimit(n) plafonne la concurrence. Il vit hors de la bibliothèque standard, donc il ne peut pas s'exécuter dans l'éditeur de cette page :
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 pool de workers
Un nombre fixe de goroutines qui lisent des tâches depuis un channel garde la concurrence bornée, quel que soit le nombre de tâches. Le WaitGroup vous dit quand tous les workers ont fini, c'est-à-dire quand le channel de résultats peut être fermé.
L'ordre des trois éléments compte :
maindoit recevoir les résultats pendant que les workers tournent. Simainappelaitwg.Wait()directement avant de lire, les workers bloqueraient en envoyant surresults, n'atteindraient jamaisDone, et tout finirait en deadlock. C'est pourquoiWaits'exécute dans sa propre goroutine.close(results)n'a lieu qu'aprèsWait, donc aucun worker ne peut envoyer sur un channel fermé.- L'alimentation des tâches s'exécute aussi dans une goroutine, donc l'alimentation et la collecte se chevauchent.
Quel worker a traité quelle tâche change d'une exécution à l'autre, donc le programme trie par tâche avant d'afficher. Tout ce qu'il affiche est déterministe.
WaitGroup, channel ou errgroup
| Besoin | À utiliser |
|---|---|
| Attendre N goroutines, résultats dans des cases indexées | sync.WaitGroup |
| Attendre une seule goroutine | un channel done ou le channel de résultat lui-même |
| Résultats transmis au fur et à mesure | un channel, fermé après wg.Wait() |
| Tout arrêter à la première erreur | errgroup.WithContext |
| Tout arrêter sur un timeout ou une annulation de l'appelant | context.Context plus un WaitGroup ou un errgroup |
Go 1.25 ajoute wg.Go(func() { ... }), qui fait le Add(1) et le Done différé à votre place. Le code pour Go 1.24 et avant, y compris l'éditeur de cette page, utilise la forme explicite montrée plus haut.
Erreurs courantes
wg.Add(1)dans la goroutine.Waitpeut retourner avant qu'il ne s'exécute.- Oublier
Donelors d'un retour anticipé. Faites toujoursdefer wg.Done(). - Passer le WaitGroup par valeur. Utilisez un pointeur ;
go vetsignale la copie. - Attendre dans la goroutine même qui doit vider un channel. Déplacez
wg.Wait()et leclosedans une goroutine séparée. - Réutiliser un WaitGroup avant le retour du
Waitprécédent. Ne commencez un nouveau cycle d'appels àAddqu'une foisWaitterminé.
Questions fréquentes
Comment fonctionne sync.WaitGroup en Go ?
Un WaitGroup est un compteur. wg.Add(n) l'augmente, wg.Done() le diminue de un, et wg.Wait() bloque jusqu'à ce qu'il atteigne zéro. Appelez Add avant de lancer chaque goroutine, defer wg.Done() à l'intérieur, et Wait là où vous avez besoin que tout soit terminé.
Faut-il passer un WaitGroup par valeur ou par pointeur ?
Par pointeur (*sync.WaitGroup), ou laissez les goroutines le capturer dans une closure. Une copie a son propre compteur, donc Done sur la copie n'atteint jamais l'original et Wait bloque indéfiniment. go vet signale l'erreur par « passes lock by value ».
Qu'est-ce qui cause « sync: negative WaitGroup counter » ?
Plus d'appels à Done que d'appels à Add. En général, une goroutine appelle Done deux fois (une fois avec defer et une fois explicitement), ou Add(1) est sauté sur un chemin. Le programme provoque un panic, puisque le compteur ne peut plus rien dire de vrai.
Comment récupérer les erreurs des goroutines lancées avec un WaitGroup ?
Un WaitGroup ne transporte ni résultats ni erreurs. Donnez à chaque goroutine sa propre case dans une slice d'erreurs et combinez-les après Wait (par exemple avec errors.Join), ou utilisez golang.org/x/sync/errgroup, dont le Wait renvoie la première erreur et peut annuler les autres via un context.