Menu

Golang select: Timeouts, default und Quit-Channels

Wie select auf mehrere Channel-Operationen gleichzeitig wartet: Auswahl unter bereiten Cases, nicht blockierendes Senden und Empfangen mit default, Timeouts mit time.After und Schleifen mit einem Quit-Channel oder Context beenden.

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

Auf mehrere Channels warten

select sieht aus wie ein switch, aber jeder Case ist ein Senden oder Empfangen auf einem Channel. Es blockiert, bis ein Case fortfahren kann, und führt dann diesen aus.

Mit diesen Verzögerungen bekommt das erste select das schnelle Ergebnis und das zweite das langsame. Keiner der beiden Empfänge muss auf den anderen Channel warten. Ein einfaches <-slow gefolgt von <-fast würde sie in fester Reihenfolge abarbeiten, egal welches zuerst ankommt.

Wie select auswertet:

  1. Alle Channel-Ausdrücke und die zu sendenden Werte werden einmal in Reihenfolge des Quelltexts ausgewertet, wenn das select beginnt.
  2. Sind ein oder mehrere Cases bereit, wird einer davon zufällig gewählt.
  3. Ist keiner bereit und gibt es ein default, läuft das default.
  4. Andernfalls blockiert die Goroutine, bis ein Case bereit wird.

Ein leeres select {} blockiert für immer. Du siehst es gelegentlich am Ende von main in Programmen, deren eigentliche Arbeit in anderen Goroutinen passiert.

Zufällige Wahl unter bereiten Cases

Sind mehrere Cases gleichzeitig bereit, bevorzugt select nicht den zuerst aufgeführten. Dieses Programm füllt zwei gepufferte Channels und führt dann 1000 Mal ein select aus:

Die Aufteilung zwischen countA und countB ändert sich bei jedem Lauf und liegt jeweils bei etwa 500. Die zufällige Wahl ist Absicht: Sie verhindert, dass ein viel beschäftigter Channel die anderen aushungert. Brauchst du Priorität, sieh dir das Muster weiter unten an.

Nicht blockierende Operationen mit default

Mit einem default-Case blockiert select nie. So wird ein Senden oder Empfangen zu einem Versuch:

Arbeit zu verwerfen, wenn ein Puffer voll ist, ist die Art, wie du Last abwirfst oder Metriken ausgibst, ohne den Aufrufer je aufzuhalten.

Setz kein default in ein select innerhalb einer for-Schleife, nur um Channels immer wieder zu „prüfen“. Ist nichts bereit, dreht die Schleife mit 100 % CPU durch. Blockiere stattdessen und füg einen Timeout-Case hinzu, wenn du regelmäßig aufwachen musst.

Timeouts

time.After(d) gibt einen Channel zurück, der nach d einmal einen Wert empfängt. Lass ihn gegen die eigentliche Arbeit antreten:

Der erste Aufruf gibt "data" zurück. Der zweite gibt nach 50 ms den Timeout-Fehler zurück, lange bevor der Worker fertig gewesen wäre. Der Ergebnis-Channel hat absichtlich einen Puffer von 1. Gewinnt der Timeout, empfängt niemand mehr aus result; bei einem ungepufferten Channel würde die Worker-Goroutine für immer beim Senden blockieren und leaken.

In einer Schleife erzeugt time.After bei jedem Durchlauf einen neuen Timer. Das ist genau richtig für einen Leerlauf-Timeout pro Nachricht („1 Sekunde lang keine Nachricht“). Für eine Gesamt-Deadline über viele Operationen erzeugst du einen Timer oder Context vor der Schleife. Seit Go 1.23 werden Timer, auf die nichts mehr verweist, vom Garbage Collector eingesammelt, auch wenn sie noch nicht ausgelöst haben. time.After in einer Schleife hält also nicht mehr Speicher, bis jeder Timer auslöst, wie in älteren Versionen (dafür braucht es go 1.23 oder höher in go.mod).

for-select-Schleifen und Quit-Channels

Eine Goroutine, die läuft, bis sie gestoppt wird, ist eine for-Schleife um ein select mit einem Case für die Arbeit und einem für das Stoppen:

quit zu schließen statt darauf zu senden ist das Idiom: Ein Schließen sehen alle Empfänger, jetzt und später, also stoppt ein einziges close beliebig viele Worker. Mit dem Channel done kann main warten, bis der Worker wirklich zurückgekehrt ist.

In echtem Code ist der Quit-Channel meist ein context.Context: case <-ctx.Done():. Das funktioniert genauso (Done() gibt einen Channel zurück, der beim Abbruch geschlossen wird) und transportiert zusätzlich Deadlines und den Grund für das Stoppen. Die Seite zu Context behandelt das.

break innerhalb von select

break in einem select-Case verlässt das select, nicht das umschließende for. Das ist eine häufige Quelle für Schleifen, die nie enden. Nimm return oder setz ein Label an die Schleife:

Priorität zwischen Channels

Da select zufällig wählt, kannst du Cases innerhalb einer Anweisung nicht gewichten. Soll ein Channel immer gewinnen, wenn er etwas hat, prüf ihn zuerst für sich allein:

for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	select {
	case <-ctx.Done():
		return ctx.Err()
	case job := <-jobs:
		handle(job)
	}
}

Das erste select kehrt sofort zurück, wenn der Abbruch schon passiert ist. Ohne es könnte ein stetiger Strom von Jobs die zufällige Wahl noch eine Weile nach dem Abbruch von ctx gewinnen.

nil-Channels schalten einen Case ab

Ein Senden oder Empfangen auf einem nil-Channel ist nie bereit, also ist ein Case auf einem nil-Channel praktisch abgeschaltet. Einen geschlossenen Eingang auf nil zu setzen ist der Weg, nicht mehr darauf zu warten und mit den anderen weiterzumachen; die Seite zu Channels zeigt eine Merge-Schleife, die so gebaut ist. Mit demselben Trick schaltest du einen Timeout ein und aus: Lass var timeout <-chan time.Time auf nil, bis du ihn brauchst, und weise dann time.After(d) zu.

Häufige Fehler

  • Reihenfolge des Quelltexts erwarten. Der zuerst aufgeführte Case wird nicht bevorzugt.
  • Eine Busy-Loop mit default. Ein for { select { ... default: } } ohne Arbeit verbrennt einen CPU-Kern.
  • Den Verlierer leaken. Gewinnt ein Timeout, muss die Goroutine, die das Ergebnis gesendet hätte, trotzdem fertig werden können. Gib ihrem Channel einen Puffer von 1.
  • break, das nur das select verlässt. Nimm ein Label oder return.
  • Ein time.After pro Schleifendurchlauf als Gesamt-Deadline. Es startet bei jedem Durchlauf neu; erzeuge die Deadline einmal außerhalb der Schleife.

Häufig gestellte Fragen

Was macht select in Go?

select wartet, bis eine seiner Channel-Operationen (ein Senden oder ein Empfangen) fortfahren kann, und führt dann diesen Case aus. Sind mehrere gleichzeitig bereit, wählt es zufällig einen aus. Ohne default-Case blockiert es, bis ein Case bereit ist; mit default blockiert es nie.

Wie füge ich in Go einem Channel-Empfang einen Timeout hinzu?

Setz den Empfang und einen Timer in ein gemeinsames select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Was zuerst passiert, gewinnt. In einer Schleife oder wenn ein Aufrufer schon eine Deadline hat, nimm stattdessen einen context.Context mit context.WithTimeout und warte per select auf ctx.Done().

Wählt select in Go die Cases der Reihe nach?

Nein. Sind mehrere Cases bereit, wählt Go gleichverteilt zufällig, damit kein Case die anderen aushungern kann. Brauchst du Priorität, prüf den wichtigen Channel zuerst in einem eigenen select mit default und geh dann zu einem select über alle Channels weiter.

Warum verlässt break meine for-select-Schleife nicht?

Innerhalb eines select verlässt break nur die select-Anweisung, nicht das umgebende for. Nimm return oder setz ein Label an die Schleife (loop: for { select { case <-done: break loop } }).

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S