Das Grundmuster
sync.WaitGroup zählt laufende Goroutinen. Add erhöht den Zähler, Done verringert ihn, Wait blockiert, bis er null ist.
Die drei Downloads laufen nebenläufig, also braucht das Programm etwa 10 ms statt 30. Die Ergebnisse erscheinen in der Reihenfolge der Eingabe, weil jede Goroutine nur ihren eigenen Index von sizes schreibt und main sie erst nach Wait liest.
Der Nullwert einer WaitGroup ist einsatzbereit. Kein Konstruktor nötig.
Die drei Regeln
Ruf Add vor go auf, nicht in der Goroutine. Ruft die Goroutine Add selbst auf, kann main bei Wait ankommen, bevor irgendeine Goroutine gestartet ist, einen Zähler von null sehen und zurückkehren, obwohl die Arbeit noch nicht begonnen hat. Kennst du die Anzahl vorher, ist ein einmaliges wg.Add(len(files)) vor der Schleife gleichwertig.
Ruf Done mit defer in der ersten Zeile der Goroutine auf. Auch eine Goroutine, die bei einem Fehler früh zurückkehrt oder panict, verringert dann den Zähler. Ein fehlendes Done lässt Wait für immer blockieren. Ist es die letzte verbliebene Goroutine, meldet die Runtime fatal error: all goroutines are asleep mit sync.WaitGroup.Wait im Trace.
Kopier eine WaitGroup nach der ersten Verwendung nie. Übergib *sync.WaitGroup an Funktionen oder fang die Variable wie oben in einer Closure ein.
Eine WaitGroup an eine Funktion übergeben
Ist der Rumpf der Goroutine eine benannte Funktion, übergib einen Pointer:
Mit wg sync.WaitGroup als Wertparameter würde jeder Worker Done auf seiner eigenen Kopie aufrufen, und main würde für immer in Wait blockieren. go vet findet das, bevor du irgendetwas ausführst:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Ein saubereres Design hält die Nebenläufigkeit ganz aus worker heraus: Lass es eine einfache Funktion sein und erledige die Buchführung mit Add/Done in der Closure des Aufrufers. Dann ist worker leicht zu testen und synchron aufzurufen.
Negativer Zähler
Done ist Add(-1). Fällt der Zähler unter null, löst das Programm eine Panic aus:
Die Ausgabe ist recovered: sync: negative WaitGroup counter. Die übliche Ursache ist eine Goroutine mit defer wg.Done(), die auf irgendeinem Pfad zusätzlich explizit wg.Done() aufruft.
Fehler einsammeln
Eine WaitGroup zählt nur. Für Fehler gibst du jeder Goroutine ihren eigenen Platz und prüfst sie nach Wait:
errors.Join (Go 1.20) überspringt nil-Werte und gibt nil zurück, wenn alle nil sind. Damit fasst es „ein Fehler pro Goroutine“ ohne zusätzliche Buchführung zusammen.
Willst du die restliche Arbeit abbrechen, sobald eine Goroutine fehlschlägt, nimm stattdessen golang.org/x/sync/errgroup. Das ist eine WaitGroup plus der erste Fehler plus ein Context, der bei einem Fehlschlag abgebrochen wird, und g.SetLimit(n) begrenzt die Nebenläufigkeit. Es liegt außerhalb der Standardbibliothek und läuft daher nicht im Editor dieser Seite:
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
}
Ein Worker-Pool
Eine feste Anzahl von Goroutinen, die Jobs aus einem Channel lesen, hält die Nebenläufigkeit begrenzt, egal wie viele Jobs es gibt. Die WaitGroup sagt dir, wann alle Worker fertig sind, und genau dann kann der Ergebnis-Channel geschlossen werden.
Die Reihenfolge der drei Teile ist wichtig:
mainmuss Ergebnisse empfangen, während die Worker laufen. Würdemainwg.Wait()direkt vor dem Lesen aufrufen, würden die Worker beim Senden aufresultsblockieren, nieDoneerreichen, und alles würde im Deadlock enden. Deshalb läuftWaitin einer eigenen Goroutine.close(results)passiert erst nachWait, also kann kein Worker auf einen geschlossenen Channel senden.- Auch das Einspeisen der Jobs läuft in einer Goroutine, sodass sich Einspeisen und Einsammeln überlappen.
Welcher Worker welchen Job bearbeitet, ändert sich von Lauf zu Lauf, also sortiert das Programm vor der Ausgabe nach Job. Alles, was es ausgibt, ist deterministisch.
WaitGroup, Channel oder errgroup
| Bedarf | Nimm |
|---|---|
| Auf N Goroutinen warten, Ergebnisse in indizierten Plätzen | sync.WaitGroup |
| Auf eine einzige Goroutine warten | einen done-Channel oder den Ergebnis-Channel selbst |
| Ergebnisse streamen, sobald sie fertig sind | einen Channel, geschlossen nach wg.Wait() |
| Beim ersten Fehler alles stoppen | errgroup.WithContext |
| Bei Timeout oder Abbruch durch den Aufrufer alles stoppen | context.Context plus WaitGroup oder errgroup |
Go 1.25 führt wg.Go(func() { ... }) ein, das Add(1) und das per defer aufgerufene Done für dich erledigt. Code für Go 1.24 und älter, einschließlich des Editors dieser Seite, verwendet die oben gezeigte explizite Form.
Häufige Fehler
wg.Add(1)innerhalb der Goroutine.Waitkann zurückkehren, bevor es läuft.Donebei einem frühen Return vergessen. Immerdefer wg.Done().- Die WaitGroup als Wert übergeben. Nimm einen Pointer;
go vetmarkiert die Kopie. - In derselben Goroutine warten, die einen Channel leeren muss. Verschieb
wg.Wait()samtclosein eine separate Goroutine. - Eine WaitGroup wiederverwenden, bevor das vorherige
Waitzurückgekehrt ist. Beginne einen neuen Zyklus vonAdd-Aufrufen erst, wennWaitfertig ist.
Häufig gestellte Fragen
Wie funktioniert sync.WaitGroup in Go?
Eine WaitGroup ist ein Zähler. wg.Add(n) erhöht ihn, wg.Done() verringert ihn um eins, und wg.Wait() blockiert, bis er null erreicht. Ruf Add vor dem Start jeder Goroutine auf, defer wg.Done() in ihr und Wait dort, wo alles fertig sein muss.
Sollte ich eine WaitGroup als Wert oder per Pointer übergeben?
Per Pointer (*sync.WaitGroup), oder lass Goroutinen sie in einer Closure einfangen. Eine Kopie hat ihren eigenen Zähler, also erreicht Done auf der Kopie nie das Original, und Wait blockiert für immer. go vet meldet den Fehler als „passes lock by value“.
Was verursacht „sync: negative WaitGroup counter“?
Mehr Aufrufe von Done als von Add. Meist ruft eine Goroutine Done zweimal auf (einmal mit defer und einmal explizit), oder Add(1) wird auf einem Pfad übersprungen. Das Programm löst eine Panic aus, da der Zähler nichts Wahres mehr aussagen kann.
Wie bekomme ich Fehler aus Goroutinen, die mit einer WaitGroup gestartet wurden?
Eine WaitGroup transportiert weder Ergebnisse noch Fehler. Gib jeder Goroutine ihren eigenen Platz in einem Slice von Fehlern und fasse sie nach Wait zusammen (zum Beispiel mit errors.Join), oder nimm golang.org/x/sync/errgroup, dessen Wait den ersten Fehler zurückgibt und die anderen über einen Context abbrechen kann.