ErrGroup: Go's Hidden Gem for Concurrent Programming
Olivia Novak
Dev Intern · Leapcell

Go Language errgroup Library: A Powerful Concurrency Control Tool
errgroup
ist ein Utility in der offiziellen Go-Bibliothek x
, das zum gleichzeitigen Ausführen mehrerer Goroutinen
und zur Fehlerbehandlung verwendet wird. Es implementiert errgroup.Group
basierend auf sync.WaitGroup
und bietet leistungsfähigere Funktionen für die nebenläufige Programmierung.
Vorteile von errgroup
Im Vergleich zu sync.WaitGroup
hat errgroup.Group
die folgenden Vorteile:
- Fehlerbehandlung:
sync.WaitGroup
ist nur dafür verantwortlich, auf die Fertigstellung derGoroutinen
zu warten und behandelt keine Rückgabewerte oder Fehler. Währenderrgroup.Group
Rückgabewerte nicht direkt verarbeiten kann, kann es andere laufendeGoroutinen
sofort abbrechen, wenn eineGoroutine
auf einen Fehler stößt, und den ersten Nicht-nil
-Fehler in derWait
-Methode zurückgeben. - Kontextabbruch:
errgroup
kann in Verbindung mitcontext.Context
verwendet werden. Wenn eineGoroutine
auf einen Fehler stößt, kann sie automatisch andereGoroutinen
abbrechen, wodurch Ressourcen effektiv kontrolliert und unnötige Arbeit vermieden wird. - Vereinfachung der nebenläufigen Programmierung: Die Verwendung von
errgroup
kann den Boilerplate-Code für die Fehlerbehandlung reduzieren. Entwickler müssen Fehlerzustände und Synchronisationslogik nicht manuell verwalten, was die nebenläufige Programmierung einfacher und wartbarer macht. - Begrenzung der Anzahl der Parallelität:
errgroup
bietet eine Schnittstelle, um die Anzahl der parallelenGoroutinen
zu begrenzen, um eine Überlastung zu vermeiden. Dies ist eine Funktion, diesync.WaitGroup
nicht bietet.
Beispiel für die Verwendung von sync.WaitGroup
Bevor wir errgroup.Group
vorstellen, wollen wir zuerst die Verwendung von sync.WaitGroup
wiederholen.
package main import ( "fmt" "net/http" "sync" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } var err error var wg sync.WaitGroup for _, url := range urls { wg.Add(1) go func() { defer wg.Done() resp, e := http.Get(url) if e != nil { err = e return } defer resp.Body.Close() fmt.Printf("fetch url %s status %s\n", url, resp.Status) }() } wg.Wait() if err != nil { fmt.Printf("Error: %s\n", err) } }
Ausführungsergebnis:
$ go run waitgroup/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
Typische Redewendung von sync.WaitGroup
:
var wg sync.WaitGroup for ... { wg.Add(1) go func() { defer wg.Done() // do something }() } wg.Wait()
Beispiel für die Verwendung von errgroup.Group
Grundlegende Verwendung
Das Verwendungsmuster von errgroup.Group
ist ähnlich dem von sync.WaitGroup
.
package main import ( "fmt" "net/http" "golang.org/x/sync/errgroup" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } var g errgroup.Group for _, url := range urls { g.Go(func() error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() fmt.Printf("fetch url %s status %s\n", url, resp.Status) return nil }) } if err := g.Wait(); err != nil { fmt.Printf("Error: %s\n", err) } }
Ausführungsergebnis:
$ go run examples/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
Kontextabbruch
errgroup
bietet errgroup.WithContext
, um eine Abbruchfunktion hinzuzufügen.
package main import ( "context" "fmt" "net/http" "sync" "golang.org/x/sync/errgroup" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } g, ctx := errgroup.WithContext(context.Background()) var result sync.Map for _, url := range urls { g.Go(func() error { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() result.Store(url, resp.Status) return nil }) } if err := g.Wait(); err != nil { fmt.Println("Error: ", err) } result.Range(func(key, value any) bool { fmt.Printf("fetch url %s status %s\n", key, value) return true }) }
Ausführungsergebnis:
$ go run examples/withcontext/main.go
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
fetch url http://www.google.com/ status 200 OK
Da die Anfrage an http://www.somestupidname.com/ einen Fehler meldete, hat das Programm die Anfrage an http://www.golang.org/ abgebrochen.
Begrenzung der Anzahl der Parallelität
errgroup
bietet errgroup.SetLimit
, um die Anzahl der gleichzeitig ausgeführten Goroutinen
zu begrenzen.
package main import ( "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group g.SetLimit(3) for i := 1; i <= 10; i++ { g.Go(func() error { fmt.Printf("Goroutine %d is starting\n", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d is done\n", i) return nil }) } if err := g.Wait(); err != nil { fmt.Printf("Encountered an error: %v\n", err) } fmt.Println("All goroutines complete.") }
Ausführungsergebnis:
$ go run examples/setlimit/main.go
Goroutine 3 is starting
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 is done
Goroutine 1 is done
Goroutine 5 is starting
Goroutine 3 is done
Goroutine 6 is starting
Goroutine 4 is starting
Goroutine 6 is done
Goroutine 5 is done
Goroutine 8 is starting
Goroutine 4 is done
Goroutine 7 is starting
Goroutine 9 is starting
Goroutine 9 is done
Goroutine 8 is done
Goroutine 10 is starting
Goroutine 7 is done
Goroutine 10 is done
All goroutines complete.
Versuch zu starten
errgroup
bietet errgroup.TryGo
, um zu versuchen, eine Aufgabe zu starten, die in Verbindung mit errgroup.SetLimit
verwendet werden muss.
package main import ( "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group g.SetLimit(3) for i := 1; i <= 10; i++ { if g.TryGo(func() error { fmt.Printf("Goroutine %d is starting\n", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d is done\n", i) return nil }) { fmt.Printf("Goroutine %d started successfully\n", i) } else { fmt.Printf("Goroutine %d could not start (limit reached)\n", i) } } if err := g.Wait(); err != nil { fmt.Printf("Encountered an error: %v\n", err) } fmt.Println("All goroutines complete.") }
Ausführungsergebnis:
$ go run examples/trygo/main.go
Goroutine 1 started successfully
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 started successfully
Goroutine 3 started successfully
Goroutine 4 could not start (limit reached)
Goroutine 5 could not start (limit reached)
Goroutine 6 could not start (limit reached)
Goroutine 7 could not start (limit reached)
Goroutine 8 could not start (limit reached)
Goroutine 9 could not start (limit reached)
Goroutine 10 could not start (limit reached)
Goroutine 3 is starting
Goroutine 2 is done
Goroutine 3 is done
Goroutine 1 is done
All goroutines complete.
Quellcode-Interpretation
Der Quellcode von errgroup
besteht hauptsächlich aus 3 Dateien:
- Hauptlogikcode
- Implementierung von withCancelCause für Go 1.24 und höhere Versionen
- Implementierung von withCancelCause für Versionen unter Go 1.24
Kernstruktur
type token struct{} type Group struct { cancel func(error) wg sync.WaitGroup sem chan token errOnce sync.Once err error }
token
: Eine leere Struktur, die verwendet wird, um Signale zu übergeben, um die Anzahl der Parallelität zu steuern.Group
:cancel
: Die Funktion, die aufgerufen wird, wenn der Kontext abgebrochen wird.wg
: Die intern verwendetesync.WaitGroup
.sem
: Der Signalkanal, der die Anzahl der parallelen Koroutinen steuert.errOnce
: Stellt sicher, dass der Fehler nur einmal behandelt wird.err
: Zeichnet den ersten Fehler auf.
Hauptmethoden
- SetLimit: Begrenzt die Anzahl der Parallelität.
func (g *Group) SetLimit(n int) { if n < 0 { g.sem = nil return } if len(g.sem) != 0 { panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) } g.sem = make(chan token, n) }
- Go: Startet eine neue Koroutine, um die Aufgabe auszuführen.
func (g *Group) Go(f func() error) { if g.sem != nil { g.sem <- token{} } g.wg.Add(1) go func() { defer g.done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel(g.err) } }) } }() }
- Wait: Wartet, bis alle Aufgaben abgeschlossen sind, und gibt den ersten Fehler zurück.
func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel(g.err) } return g.err }
- TryGo: Versucht, eine Aufgabe zu starten.
func (g *Group) TryGo(f func() error) bool { if g.sem != nil { select { case g.sem <- token{}: default: return false } } g.wg.Add(1) go func() { defer g.done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel(g.err) } }) } }() return true }
Fazit
errgroup
ist eine offizielle erweiterte Bibliothek, die auf der Basis von sync.WaitGroup
Fehlerbehandlungsfunktionen hinzufügt und Funktionen wie Synchronisation, Fehlerweiterleitung und Kontextabbruch bereitstellt. Seine WithContext
-Methode kann eine Abbruchfunktion hinzufügen, SetLimit
kann die Anzahl der Parallelität begrenzen, und TryGo
kann versuchen, eine Aufgabe zu starten. Der Quellcode ist raffiniert gestaltet und eine Referenz wert.
Leapcell: Die Next-Gen Serverless-Plattform für Webhosting, Async Tasks und Redis
Schließlich möchte ich die am besten geeignete Plattform für die Bereitstellung von Golang empfehlen: Leapcell
1. Multi-Language Support
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Weitere Informationen finden Sie in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ