Menu

Goroutinen in Go: wie sie funktionieren, mit Beispielen

So führst du Funktionen mit dem Schlüsselwort go nebenläufig aus, wartest, bis sie fertig sind, bekommst Ergebnisse zurück und vermeidest die Data Races, Leaks und Abstürze, die Goroutinen leicht machen.

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

Eine Goroutine starten

Setz go vor einen Funktionsaufruf, und dieser Aufruf läuft nebenläufig. Die Anweisung kehrt sofort zurück; der Aufrufer wartet nicht.

Vier Goroutinen führen shout gleichzeitig aus. In welcher Reihenfolge sie fertig werden, ist nicht festgelegt, aber die Ausgabe ist immer in der ursprünglichen Reihenfolge, weil jede Goroutine an ihren eigenen Index schreibt und main den Slice erst liest, nachdem wg.Wait() zurückgekehrt ist.

Dieses Beispiel enthält schon die drei Dinge, die fast jedes Programm mit Goroutinen braucht: einen Weg, Arbeit zu starten (go), einen Weg, darauf zu warten (sync.WaitGroup), und einen Weg, Ergebnisse zurückzubekommen, ohne dass zwei Goroutinen denselben Speicher anfassen (je ein Slice-Element).

main wartet nicht

Wenn main zurückkehrt, beendet sich das Programm. Goroutinen, die noch laufen, werden gestoppt, wo auch immer sie gerade sind. Nichts wartet auf sie.

Das gibt meist nur from main aus. Manchmal wird die Goroutine rechtzeitig eingeplant, und du siehst beide Zeilen. Genau dieses „meist“ ist das Problem: Code, der auf deinem Rechner funktioniert und auf einem ausgelasteten Server scheitert.

Ein time.Sleep am Ende von main lässt die Demo beide Zeilen ausgeben, und es ist die falsche Lösung. Es rät, wie lange die Arbeit dauert. Warte auf die Arbeit selbst, mit einer WaitGroup (die Seite zu WaitGroup behandelt sie im Detail) oder mit einem Channel.

Ergebnisse zurückbekommen

Eine go-Anweisung wirft die Rückgabewerte der Funktion weg. x := go f() kompiliert nicht. Es gibt zwei Standardwege, Daten zurückzugeben.

Ein Platz pro Goroutine, wie im ersten Beispiel. Dimensioniere einen Slice vorab, gib jeder Goroutine ihren Index und lies nach Wait. Die Reihenfolge bleibt erhalten, und du brauchst kein Lock, weil keine zwei Goroutinen dasselbe Element schreiben.

Ein Channel. Jede Goroutine sendet ihr Ergebnis; der Empfänger sammelt sie ein. Die Ergebnisse kommen in der Reihenfolge an, in der die Goroutinen fertig werden, nicht in der Startreihenfolge.

Genau len(nums) Werte zu empfangen dient zugleich als Warten: main kommt nicht über die Schleife hinaus, bevor jede Goroutine gesendet hat. Die Ankunftsreihenfolge ändert sich zwischen den Läufen, also sortiert das Programm, bevor es etwas Reihenfolgeabhängiges ausgibt. Die Seite zu Channels behandelt gepufferte Channels, das Schließen und range über einen Channel.

Schleifenvariablen und Closures (Änderung in Go 1.22)

Seit Go 1.22 bekommt jeder Durchlauf einer for-Schleife eine neue Kopie der Schleifenvariablen. Eine Closure, die in einer Goroutine gestartet wird, fängt den Wert dieses Durchlaufs ein, also ist das hier korrekt:

for i, w := range words {
	go func() {
		results[i] = shout(w) // Go 1.22+: i and w belong to this iteration
	}()
}

Vor Go 1.22 teilten sich alle Durchläufe ein i und ein w, und jede Goroutine sah tendenziell den letzten Wert. Alter Code umgeht das, indem er die Werte als Argumente übergibt, go func(i int, w string) { ... }(i, w), oder durch Verschatten, i := i. Beides schadet ab Go 1.22 nicht, und du siehst es in bestehendem Code noch. Das neue Verhalten gilt, wenn die go.mod des Moduls go 1.22 oder höher angibt.

Goroutinen sind billig

Eine Goroutine startet mit einem kleinen Stack (ein paar Kilobyte), den die Runtime bei Bedarf wachsen und schrumpfen lässt. Der Go-Scheduler führt Goroutinen auf einem Pool von OS-Threads aus, von denen höchstens GOMAXPROCS gleichzeitig Go-Code ausführen, und standardmäßig ist GOMAXPROCS die Anzahl der CPUs. Blockiert eine Goroutine auf einem Channel, einem Mutex, einem Sleep oder Netzwerk-I/O, wird sie geparkt, und der Thread ist frei für eine andere.

Eine Goroutine pro Aufgabe zu starten ist also auch in großer Zahl in Ordnung:

Hunderttausend Goroutinen sind in einem Bruchteil einer Sekunde fertig. Die Summe ist immer 4999950000, weil atomic.Int64 jede Addition unteilbar macht. Billig heißt aber nicht kostenlos: Jede Goroutine, die noch blockiert ist, hält ihren Stack und alles, worauf sie verweist, am Leben.

OS-ThreadGoroutine
Erzeugt vondem Kernelder Go-Runtime
Anfangs-Stackfest, oft 1 MB oder mehrein paar KB, wächst bei Bedarf
WechselKontextwechsel im KernelGo-Scheduler, im User Space
Identitäthat eine Thread-IDabsichtlich keine lesbare ID
Typische AnzahlHunderteTausende bis Millionen

Data Races

Greifen zwei Goroutinen gleichzeitig auf dieselbe Variable zu und schreibt mindestens eine davon, ist das ein Data Race. Das Ergebnis ist unvorhersehbar, nicht nur „ein bisschen daneben“: Aktualisierungen gehen verloren, und ein Race auf einem String, Slice, einer Map oder einem Interface-Wert kann das Programm abstürzen lassen oder den Speicher beschädigen.

Auf einem Mehrkernrechner gibt das bei den meisten Läufen eine andere Zahl unter 10000 aus, weil zwei Goroutinen denselben alten Wert lesen und beide diesen Wert plus eins zurückschreiben. Auf einem einzelnen Kern kann es 10000 ausgeben, und das ist schlimmer: Der Bug besteht deinen Test und taucht in Produktion auf.

Go liefert einen Race Detector mit. Führ dein Programm oder deine Tests mit -race aus:

go run -race main.go
go test -race ./...
==================
WARNING: DATA RACE
Read at 0x00c000090038 by goroutine 8:
  main.main.func1()
      /tmp/race/main.go:16 +0x94

Previous write at 0x00c000090038 by goroutine 6:
  main.main.func1()
      /tmp/race/main.go:16 +0xa4
...
Found 2 data race(s)
exit status 66

Er zeigt auf die genaue Zeile (counter++) und beide Goroutinen. Er meldet nur Races, die während des Laufs tatsächlich passieren, also lass ihn über Tests laufen, die die nebenläufigen Pfade durchspielen. Er verlangsamt das Programm um ein Mehrfaches, ist also für Tests und Staging gedacht, nicht für die Produktion.

Die Lösungen, von der einfachsten zur allgemeinsten:

  • Nicht teilen. Gib jeder Goroutine ihre eigenen Daten und führe sie am Ende zusammen (das Muster ein Platz pro Goroutine).
  • Nimm sync/atomic für einen einzelnen Zähler oder ein Flag: var n atomic.Int64; n.Add(1).
  • Nimm einen sync.Mutex um alles Größere, etwa eine Map oder ein Struct mit mehreren Feldern. Die Seite zu Mutex behandelt auch RWMutex und sync.Once.
  • Sende die Daten über einen Channel, sodass immer nur eine Goroutine sie besitzt.

Eine Panic in einer Goroutine beendet das Programm

Löst eine Goroutine eine Panic aus und fängt nichts in derselben Goroutine sie ab, stürzt das ganze Programm ab, einschließlich main und aller anderen Goroutinen. Ein recover in main hilft nicht, weil recover nur Panics in seiner eigenen Goroutine abfängt.

So abzufangen ergibt am Rand eines lange laufenden Servers Sinn, wo ein fehlerhafter Request nicht den Rest lahmlegen darf. In gewöhnlichem Code bedeutet eine Panic meist einen Bug, und ein lauter Absturz ist das richtige Ergebnis.

Goroutine-Leaks

Eine Goroutine, die für immer blockiert, endet nie und gibt ihren Speicher nie frei. Die klassische Ursache ist ein Senden, das niemand je empfangen wird:

func firstResult(urls []string) string {
	ch := make(chan string) // unbuffered
	for _, u := range urls {
		go func() { ch <- fetch(u) }()
	}
	return <-ch // takes the first result; the other senders block forever
}

Jeder Aufruf leakt len(urls) - 1 Goroutinen. In einem Server, der diesen Request tausendfach bearbeitet, steigt der Speicher, bis der Prozess stirbt. Zwei Lösungen: Mach den Channel groß genug, damit jeder Sender fertig werden kann (make(chan string, len(urls))), oder gib den Goroutinen einen Weg aufzugeben, meist einen context.Context plus ein select auf ctx.Done(). In Tests kannst du Leaks mit runtime.NumGoroutine() beobachten.

Begrenzen, wie viele gleichzeitig laufen

„Eine Goroutine pro Element“ ist für 10.000 billige Berechnungen in Ordnung. Für 10.000 HTTP-Requests an denselben Server oder 10.000 offene Dateien ist es das nicht. Begrenze die Nebenläufigkeit mit einem gepufferten Channel als Semaphor:

Der gepufferte Channel fasst höchstens 3 Tokens, also sind in jedem Moment höchstens 3 Goroutinen hinter der Zeile sem <-. Der Spitzenwert kann nie über 3 liegen, und mit zwölf Aufgaben, die jeweils schlafen, erreicht er in der Praxis 3. Ein fester Pool von Worker-Goroutinen, die aus einem Job-Channel lesen, ist die andere verbreitete Form; die Seite zu WaitGroup baut einen.

Außerhalb der Standardbibliothek vereint golang.org/x/sync/errgroup eine WaitGroup, den ersten Fehler, den Abbruch per Context und ein Limit für die Nebenläufigkeit (g.SetLimit(n)) in einem Typ. In Produktionscode, der alle vier braucht, ist das die übliche Wahl.

Häufige Fehler

  • Das Warten vergessen. main kehrt zurück, und die Arbeit passiert still nie. Jede go-Anweisung braucht einen passenden Weg, um zu erfahren, dass sie fertig ist.
  • wg.Add innerhalb der Goroutine aufrufen. Wait kann vor Add laufen, einen Zähler von null sehen und früh zurückkehren. Ruf Add vor der go-Anweisung auf.
  • Eine Variable ohne Synchronisation teilen. Maps sind der häufige Fall: Nebenläufige Schreibzugriffe auf eine Map erkennt die Runtime meist und bricht das Programm mit fatal error: concurrent map writes ab, was recover nicht abfangen kann.
  • Eine Reihenfolge annehmen. Goroutinen laufen in der Reihenfolge, die der Scheduler wählt. Muss die Ausgabe geordnet sein, sammle und sortiere, oder schreib in indizierte Plätze.
  • Mit time.Sleep synchronisieren. Das macht Tests langsam und trotzdem unzuverlässig. Warte auf das Ereignis, nicht auf eine Schätzung.
  • Eine Goroutine ohne Möglichkeit zum Stoppen starten. Alles, was in einer Schleife läuft oder auf I/O wartet, sollte einen context.Context nehmen, damit der Aufrufer es abbrechen kann.

Häufig gestellte Fragen

Was ist eine Goroutine in Go?

Eine Goroutine ist ein Funktionsaufruf, der nebenläufig zum Rest des Programms läuft. Du startest eine, indem du go vor einen Aufruf setzt: go work(). Goroutinen werden von der Go-Runtime verwaltet, nicht vom Betriebssystem, und die Runtime verteilt viele davon auf eine kleine Zahl von OS-Threads, also sind Tausende gestartete Goroutinen normal.

Wie warte ich in Go, bis Goroutinen fertig sind?

Mit einer sync.WaitGroup: Ruf vor jeder go-Anweisung wg.Add(1) auf, defer wg.Done() am Anfang der Goroutine und wg.Wait() dort, wo alle fertig sein müssen. Erzeugen die Goroutinen Werte, funktioniert auch das Empfangen eines Werts pro Goroutine aus einem Channel als Warten.

Was ist der Unterschied zwischen einer Goroutine und einem Thread?

Ein OS-Thread hat einen festen Stack (oft 1 MB oder mehr) und wird vom Kernel geplant. Eine Goroutine startet mit einem Stack von ein paar Kilobyte, der bei Bedarf wächst, und der Go-Scheduler wechselt im User Space zwischen Goroutinen. Die Runtime führt Goroutinen auf bis zu GOMAXPROCS Threads gleichzeitig aus (standardmäßig die Anzahl der CPUs).

Wie bekomme ich einen Rückgabewert aus einer Goroutine?

Eine go-Anweisung verwirft die Rückgabewerte der Funktion. Sende das Ergebnis über einen Channel (results <- compute(x)) oder schreib es in deinen eigenen Platz eines vorab dimensionierten Slices (out[i] = compute(x)) und lies es nach wg.Wait().

Warum beendet sich mein Go-Programm, bevor die Goroutine etwas ausgibt?

Wenn main zurückkehrt, endet das Programm, und jede andere Goroutine wird gestoppt, ohne ihren restlichen Code auszuführen. Nichts wartet automatisch auf Goroutinen. Blockiere main, bis die Arbeit erledigt ist, mit einer WaitGroup oder einem Channel-Empfang. Ein time.Sleep versteckt das Problem nur.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S