Menu

Golang WaitGroup: Add, Done, Wait und ein Worker-Pool

Wie sync.WaitGroup darauf wartet, dass eine Gruppe von Goroutinen fertig wird: die Regeln für Add, Done und Wait, warum sie per Pointer übergeben werden muss, Ergebnisse und Fehler einsammeln und ein Worker-Pool darauf aufgebaut.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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:

  • main muss Ergebnisse empfangen, während die Worker laufen. Würde main wg.Wait() direkt vor dem Lesen aufrufen, würden die Worker beim Senden auf results blockieren, nie Done erreichen, und alles würde im Deadlock enden. Deshalb läuft Wait in einer eigenen Goroutine.
  • close(results) passiert erst nach Wait, 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

BedarfNimm
Auf N Goroutinen warten, Ergebnisse in indizierten Plätzensync.WaitGroup
Auf eine einzige Goroutine warteneinen done-Channel oder den Ergebnis-Channel selbst
Ergebnisse streamen, sobald sie fertig sindeinen Channel, geschlossen nach wg.Wait()
Beim ersten Fehler alles stoppenerrgroup.WithContext
Bei Timeout oder Abbruch durch den Aufrufer alles stoppencontext.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. Wait kann zurückkehren, bevor es läuft.
  • Done bei einem frühen Return vergessen. Immer defer wg.Done().
  • Die WaitGroup als Wert übergeben. Nimm einen Pointer; go vet markiert die Kopie.
  • In derselben Goroutine warten, die einen Channel leeren muss. Verschieb wg.Wait() samt close in eine separate Goroutine.
  • Eine WaitGroup wiederverwenden, bevor das vorherige Wait zurückgekehrt ist. Beginne einen neuen Zyklus von Add-Aufrufen erst, wenn Wait fertig 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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S