Menu

Golang Channels: gepuffert, ungepuffert, close und range

Wie Go-Channels Werte zwischen Goroutinen übergeben: ungepufferte und gepufferte Channels, Schließen und range, Richtungstypen, der Deadlock-Fehler und eine daraus gebaute Pipeline.

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

Senden und empfangen

Ein Channel ist eine typisierte Leitung zwischen Goroutinen. ch <- v sendet, <-ch empfängt. Du erzeugst einen mit make:

Der Nullwert eines Channel-Typs ist nil, also liefert dir var ch chan string ohne make einen Channel, der für immer blockiert. Erzeuge Channels immer mit make.

Ungepufferte Channels synchronisieren

make(chan T) erzeugt einen ungepufferten Channel. Ein Senden blockiert, bis ein Empfänger den Wert nimmt, und ein Empfangen blockiert, bis ein Sender einen liefert. An diesem Punkt treffen sich die beiden Goroutinen, und das macht einen ungepufferten Channel ebenso zu einem Werkzeug der Synchronisation wie zu einer Datenleitung. Wenn <-done unten zurückkehrt, weißt du, dass der Worker alles vor seinem Senden erledigt hat:

result in main zu lesen ist hier ohne Mutex sicher. Das Speichermodell von Go garantiert, dass alles, was der Worker vor dem Senden getan hat, für main nach dem passenden Empfang sichtbar ist. chan struct{} ist der idiomatische Typ für ein reines Signal, weil struct{} keinen Speicher belegt.

Gepufferte Channels

make(chan T, n) gibt dem Channel Platz für n Werte. Senden gelingt ohne Empfänger, bis der Puffer voll ist; Empfangen gelingt, bis er leer ist. Die Werte kommen in der Reihenfolge heraus, in der sie hineingingen.

Ein Puffer entkoppelt Sender und Empfänger, damit kurze Spitzen den Sender nicht aufhalten. Einen Produzenten, der dauerhaft schneller ist als sein Konsument, repariert er nicht; er verschiebt nur den Moment, in dem der Sender blockiert. Wähl eine Puffergröße aus einem Grund (die Anzahl der Sender, eine bekannte Batch-Größe), nicht um einen Deadlock verschwinden zu lassen.

len(ch) ist eine Momentaufnahme. Bis du darauf reagierst, kann eine andere Goroutine den Wert schon geändert haben, also entscheide damit nicht, ob ein Senden blockieren wird. Dafür nimmst du select mit einem default-Case.

close und range

close(ch) sagt den Empfängern, dass keine Werte mehr gesendet werden. Nach einem close:

  • werden Werte, die schon im Puffer liegen, weiterhin zugestellt,
  • gibt danach jeder Empfang sofort den Nullwert zurück,
  • meldet v, ok := <-ch ok == false,
  • endet for v := range ch.

Der erste Empfang nach close bekommt noch "last" mit ok == true. Der zweite bekommt den Nullwert "" und false.

Regeln, deren Verletzung eine Panic auslöst:

  • Senden auf einen geschlossenen Channel löst eine Panic mit send on closed channel aus,
  • einen bereits geschlossenen Channel zu schließen löst eine Panic aus,
  • einen nil-Channel zu schließen löst eine Panic aus.

Also schließt nur die sendende Seite, und das nur einmal. Bei mehreren Sendern weiß kein einzelner, wann die anderen fertig sind; lass eine separate Goroutine auf alle Sender warten (mit einer sync.WaitGroup) und den Channel schließen, nachdem Wait zurückgekehrt ist. Schließen ist nur nötig, wenn ein Empfänger auf das Ende wartet. Ein nicht geschlossener Channel, auf den nichts mehr verweist, wird wie jeder andere Wert vom Garbage Collector eingesammelt.

Richtungstypen

Eine Funktion kann deklarieren, dass sie auf einem Channel nur sendet oder nur empfängt. Der Compiler lehnt dann die andere Operation ab.

TypBedeutungErlaubt
chan Tbidirektionalsenden, empfangen, schließen
chan<- Tnur sendensenden, schließen
<-chan Tnur empfangenempfangen

Ein chan T wird implizit in jeden der eingeschränkten Typen konvertiert, wenn du ihn an eine Funktion übergibst. Deshalb kann produce oben einen <-chan int zurückgeben: Aufrufer können mit range darüber laufen, aber nicht hineinsenden oder ihn schließen. Nutz Richtungstypen bei jedem Funktionsparameter, wo sie passen. Sie dokumentieren die Zuständigkeit und machen Missbrauch zu einem Compilerfehler.

Deadlock

Wenn jede Goroutine blockiert ist und nichts eine davon wecken kann, bricht die Runtime das Programm ab:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

Der Goroutine-Dump zeigt dir, welche Operation hängt (hier chan send, sonst etwa chan receive, sync.WaitGroup.Wait, select). Die üblichen Ursachen:

  • ein Senden auf einen ungepufferten Channel, ohne dass ein Empfänger läuft,
  • range über einen Channel, der nie geschlossen wird,
  • ein WaitGroup-Zähler, der nie null erreicht,
  • zwei Goroutinen, die jeweils auf die andere warten.

Die Runtime erkennt nur den Fall, in dem alle Goroutinen hängen. In einem Server mit anderen lebenden Goroutinen (ein HTTP-Listener, ein Ticker) bringt derselbe Bug nichts zum Absturz; die blockierte Goroutine leakt einfach.

nil-Channels

Senden und Empfangen auf einem nil-Channel blockieren für immer. Das klingt nutzlos, ist aber ein Standardtrick in select: Eine Channel-Variable auf nil zu setzen schaltet ihren Case ab. Dieses Beispiel führt zwei Channels zusammen und hört auf, auf einen zu lauschen, sobald er geschlossen ist:

Ohne die Zuweisungen von nil ist ein geschlossener Channel immer bereit, und die Schleife würde auf Nullwerten durchdrehen.

Eine Pipeline

Channels lassen sich zu Pipelines verbinden: Jede Stufe ist eine Goroutine, die aus einem Channel empfängt, in den nächsten sendet und ihren Ausgang schließt, wenn ihr Eingang fertig ist.

Jede Stufe läuft nebenläufig, und die Ausgabereihenfolge ist deterministisch (1, 16, 81), weil jede Stufe eine einzelne Goroutine ist, die die Reihenfolge erhält. Das Schließen pflanzt sich fort: generate schließt, das beendet die range-Schleife von square, die schließt ihren Ausgang, und so weiter bis zu main.

Die Schwachstelle dieser Pipeline: Würde main früher aufhören zu lesen, würden die Stufen für immer beim Senden blockieren. Echte Pipelines nehmen einen context.Context oder einen done-Channel und warten mit select neben jedem Senden darauf.

Channel oder Mutex

Channels sind dafür da, die Zuständigkeit für Daten weiterzugeben und Ereignisse zu signalisieren. Ein sync.Mutex ist einfacher, um geteilten Zustand zu schützen, den viele Goroutinen direkt lesen und ändern, etwa einen Cache oder einen Zähler. Ein Struct mit einem Mutex darin ist oft klarer als eine Goroutine, die den Zustand besitzt und Anfragen über Channels bedient. Nimm, was den Code kürzer und die Zuständigkeit offensichtlich macht.

Kurzreferenz

Operationnil-Channeloffener Channelgeschlossener Channel
ch <- vblockiert für immerblockiert, bis empfangen wird oder der Puffer Platz hatPanic
<-chblockiert für immerblockiert, bis ein Wert verfügbar istgepufferte Werte, dann Nullwert
v, ok := <-chblockiert für immerok ist trueok ist false, sobald geleert
close(ch)PanicschließtPanic
len(ch), cap(ch)0, 0gepufferte Werte, Puffergrößeverbleibende Werte, Puffergröße

Häufig gestellte Fragen

Was ist der Unterschied zwischen einem gepufferten und einem ungepufferten Channel in Go?

Ein ungepufferter Channel (make(chan int)) hat keinen Speicher: Ein Senden blockiert, bis eine andere Goroutine empfängt, also ist jedes Senden zugleich eine Übergabe und ein Synchronisationspunkt. Ein gepufferter Channel (make(chan int, 3)) fasst bis zu 3 Werte; Senden blockiert nur, wenn der Puffer voll ist, und Empfangen nur, wenn er leer ist.

Was passiert, wenn man in Go aus einem geschlossenen Channel liest?

Empfänge auf einem geschlossenen Channel blockieren nie. Sie leeren zuerst die Werte, die noch im Puffer liegen, und geben danach für immer den Nullwert des Elementtyps zurück. Mit v, ok := <-ch unterscheidest du das: ok ist false, sobald der Channel geschlossen und leer ist. Eine Schleife for v := range ch stoppt an diesem Punkt.

Wer sollte in Go einen Channel schließen?

Der Sender, und nur, wenn Empfänger wissen müssen, dass keine Werte mehr kommen (zum Beispiel, um eine range-Schleife zu beenden). Senden auf einen geschlossenen Channel löst eine Panic aus, zweimaliges Schließen ebenfalls, also konkurriert ein Empfänger, der den Channel schließt, mit den Sendern. Um einen Channel freizugeben, musst du ihn nicht schließen; der Garbage Collector holt unerreichbare Channels so oder so zurück.

Was bedeutet „fatal error: all goroutines are asleep“?

Jede Goroutine im Programm blockiert auf einer Channel-Operation oder einem Lock, die nichts mehr abschließen kann, also stoppt die Runtime das Programm. Die häufigste Ursache ist ein Senden auf einen ungepufferten Channel in main, ohne dass eine andere Goroutine empfängt, oder range über einen Channel, der nie geschlossen wird.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S