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:
- Alle Channel-Ausdrücke und die zu sendenden Werte werden einmal in Reihenfolge des Quelltexts ausgewertet, wenn das
selectbeginnt. - Sind ein oder mehrere Cases bereit, wird einer davon zufällig gewählt.
- Ist keiner bereit und gibt es ein
default, läuft dasdefault. - 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. Einfor { 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 dasselectverlässt. Nimm ein Label oderreturn.- Ein
time.Afterpro 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 } }).