Ein Channel ist kein Container, sondern ein Kommunikationskanal mit Lebenszyklus. Du erzeugst ihn mit make, sendest Werte hinein, empfängst sie auf der anderen Seite — und irgendwann signalisierst du dem Empfänger: „hier kommt nichts mehr". Genau das macht close(ch). Es ist kein „Channel zerstören", kein „Speicher freigeben", keine „Verbindung trennen" — es ist ein Endsignal, das alle Empfänger auf einen Schlag sehen. Dieser Artikel arbeitet den vollständigen Lebenszyklus durch: die Spec-Semantik von close, das range-Idiom, das comma-ok-Pattern, die Producer-Convention („nur der Sender schließt"), die typischen Panic-Fälle und die Multi-Sender-Strategien aus dem Pipelines-Blog.

Was close tut — und was nicht

Bevor wir über Idiome reden, muss die Mechanik sitzen. close(ch) ist eine Built-in-Funktion mit einer einzigen Aufgabe: dem Channel zu markieren, dass keine weiteren Sende-Operationen mehr stattfinden werden. Der Channel bleibt danach lesbar — gepufferte Werte werden weiterhin ausgeliefert — aber neue ch <- v-Aufrufe sind verboten. Die Go-Spec formuliert das im Abschnitt Close präzise:

For a channel c, the built-in function close(c) records that no more values will be sent on the channel.

Beachte das Wort records. close vermerkt einen Zustand. Es löscht nichts, gibt keinen Speicher frei, schließt keine Datei. Der Garbage Collector räumt den Channel-Speicher auf, sobald keine Referenzen mehr darauf existieren — das ist unabhängig davon, ob close jemals aufgerufen wurde.

Go close-basics.go
package main

import "fmt"

func main() {
    ch := make(chan int, 3)

    ch <- 1
    ch <- 2
    ch <- 3
    close(ch) // ab jetzt: kein Send mehr, aber Recv liefert noch

    // Werte sind weiterhin lesbar.
    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
    fmt.Println(<-ch) // 3

    // Channel ist leer und geschlossen — Recv liefert Zero-Value.
    fmt.Println(<-ch) // 0
}
Output
1
2
3
0

Die letzte Zeile zeigt die zweite zentrale Spec-Regel: Recv auf einem geschlossenen, leeren Channel blockiert nicht und gibt den Zero-Value des Element-Typs zurück. Bei chan int ist das 0, bei chan string der leere String, bei chan *T ein nil-Pointer. Das ist die Grundlage des range-Idioms — der Empfänger merkt am Zero-Value allein aber nicht, ob da gerade ein echtes 0 ankam oder das Channel-Ende. Dafür gibt es das comma-ok-Pattern.

comma-ok — „war da noch ein Wert?"

Die Spec definiert eine Mehrfachzuweisungs-Form des Empfangs-Operators, die zusätzlich zum Wert ein bool zurückgibt:

The multi-valued assignment form of the receive operator reports whether a received value was sent before the channel was closed.

Lies das genau: ok ist true, solange der Wert vor dem close gesendet wurde. Sobald der Channel leer und geschlossen ist, ist ok = false und der Wert der Zero-Value. Damit kann der Empfänger zwischen „echter Wert 0" und „Channel ist durch" unterscheiden:

Go comma-ok.go
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 0    // echte 0
    ch <- 42
    close(ch)

    for {
        v, ok := <-ch
        if !ok {
            fmt.Println("Channel geschlossen, fertig")
            break
        }
        fmt.Printf("empfangen: %d\n", v)
    }
}
Output
empfangen: 0
empfangen: 42
Channel geschlossen, fertig

Ohne comma-ok hätte die Schleife bei der ersten echten 0 nicht von „Ende" unterscheiden können. In der Praxis nutzt man dieses Pattern selten direkt — das range-Idiom verpackt es eleganter. Aber wenn du im select einzelne Empfänge prüfen willst, ist v, ok := <-ch das Werkzeug der Wahl.

for v := range ch — das idiomatische Iterieren

Die for-range-Klausel ist über Channels definiert: Sie empfängt in jedem Durchlauf einen Wert und terminiert sauber, wenn der Channel geschlossen und leer ist. Die Spec sagt:

For channels, the iteration values produced are the successive values sent on the channel until the channel is closed.

Das macht den Konsumenten extrem schlank — er kümmert sich nicht um comma-ok, nicht um Abbruchbedingungen, nicht um Restwerte:

Go range-channel.go
package main

import "fmt"

// Producer: sendet 0..4 und schließt den Channel.
func produce() <-chan int {
    out := make(chan int)
    go func() {
        defer close(out) // Pflicht — sonst hängt range ewig
        for i := 0; i < 5; i++ {
            out <- i
        }
    }()
    return out
}

func main() {
    for v := range produce() {
        fmt.Println(v)
    }
    fmt.Println("Schleife terminiert sauber")
}
Output
0
1
2
3
4
Schleife terminiert sauber

Das defer close(out) ist der entscheidende Punkt. Wenn der Producer den Channel nicht schließt, bleibt for v := range out ewig blockiert — der Empfänger weiß ja nicht, dass nichts mehr kommt. Genau dafür gibt es das Endsignal. Merksatz: Jeder Producer schließt seinen Output-Channel, und zwar mit defer direkt nach dem go-Statement, in dem er sendet. Das macht das Cleanup auch bei Panics robust.

Producer-Convention — nur der Sender schließt

Die wichtigste Konvention um close herum ist eine Frage der Zuständigkeit: Wer schließt? Die Antwort der Community, des Pipelines-Blogs und jeder seriösen Go-Codebase ist eindeutig: Nur der Sender schließt. Niemals der Empfänger. Das ist keine Stilfrage, sondern ergibt sich zwingend aus der Spec — close aus dem Empfänger heraus kann den Sender in eine Send-Panic schicken.

Die Begründung ist symmetrisch zur Mechanik:

  • Der Sender weiß, wann er fertig ist. Er hat die letzte Iteration seines Producer-Loops gesehen, oder seine Datenquelle ist erschöpft, oder ein Abbruch-Signal kam an. Er ist im richtigen Moment am richtigen Ort, um close aufzurufen.
  • Der Empfänger weiß es nicht. Er sieht nur eingehende Werte. „Lange nichts gekommen" ist kein Endsignal — vielleicht ist der Producer nur langsam. Würde der Empfänger schließen, wüsste der Sender beim nächsten ch <- v plötzlich nicht, dass der Channel zu ist, und das Programm pansicht.
Go producer-convention.go
package main

import "fmt"

// Producer-Convention: der Sender besitzt den Channel
// und ist für close zuständig.
func ints(n int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out) // hier — am Ende des Producer-Lebens
        for i := 0; i < n; i++ {
            out <- i
        }
    }()
    return out
}

func main() {
    // Konsument konsumiert nur — kein close().
    sum := 0
    for v := range ints(5) {
        sum += v
    }
    fmt.Println("Summe:", sum) // 0+1+2+3+4 = 10
}
Output
Summe: 10

Beachte die Signatur: func ints(n int) <-chan int. Der Rückgabe-Typ ist ein Receive-Only-Channel. Damit garantiert das Typsystem dem Aufrufer, dass er nicht schließen kann — close auf einem <-chan T ist ein Compile-Fehler. Das ist die idiomatische Art, die Producer-Convention statisch durchzusetzen.

Die zwei Panic-Fälle

Es gibt zwei Operationen, die zur Laufzeit panicen, und beide kreisen um das Schließen:

Panic 1: close auf einem bereits geschlossenen Channel. Die Spec sagt das in einem Satz: „Closing the nil channel or closing an already-closed channel causes a run-time panic." Das ist der häufigste Bug in Multi-Sender-Szenarien — zwei Goroutines schließen unabhängig voneinander dasselbe out, eine gewinnt, die andere stirbt.

Go panic-double-close.go
package main

func main() {
    ch := make(chan int)
    close(ch)
    close(ch) // panic: close of closed channel
}

Panic 2: ch <- v auf einem geschlossenen Channel. Symmetrisch: Sobald close(ch) aufgerufen wurde, ist jeder Send-Versuch ein Programmabbruch. Recv ist erlaubt, Send nicht.

Go panic-send-closed.go
package main

func main() {
    ch := make(chan int, 1)
    close(ch)
    ch <- 42 // panic: send on closed channel
}

Beide Panics sind nicht recoverable im Sinne von „Programm läuft normal weiter" — sie reißen die Goroutine ab, und ohne recover das ganze Programm. Wer hier defensiv recover einbauen will, sollte stattdessen die Architektur reparieren: Eine korrekt aufgesetzte Producer-Convention macht beide Panics unmöglich.

nil-Channels — eine eigene Welt

nil-Channels verdienen eine eigene Betrachtung, weil ihr Verhalten asymmetrisch zu geschlossenen Channels ist. Die Spec ist hier ungewöhnlich kompakt:

A nil channel is never ready for communication.

Heißt: <-nilCh blockiert für immer, nilCh <- v blockiert für immer, close(nilCh) paniciert. Das klingt nach einem Bug-Magnet, ist aber im select ein elegantes Werkzeug — ein nil-gesetzter Channel-Case wird vom select schlicht übersprungen, was den dynamischen Aus-/Einbau von Channel-Cases erlaubt.

Go nil-channel.go
package main

import (
    "fmt"
    "time"
)

func main() {
    var nilCh chan int // nil — nicht mit make initialisiert
    done := time.After(500 * time.Millisecond)

    select {
    case v := <-nilCh:
        // Wird NIE ausgewählt — nil-Channel ist nie bereit.
        fmt.Println("nil-Recv:", v)
    case <-done:
        fmt.Println("Timeout — nilCh war nie bereit")
    }

    // close(nilCh) // würde panicen: close of nil channel
}
Output
Timeout — nilCh war nie bereit

In normalem Code ist ein nil-Channel meist ein Zeichen, dass make vergessen wurde — ein Zero-Value-Channel ist eben nicht „nutzbar" wie ein Zero-Value-Slice (nil-Slice mit append funktioniert). Bewusst auf nil gesetzt wird er hauptsächlich im Zusammenspiel mit select, das im select-Artikel ausführlich behandelt wird.

Buffer-Verhalten — gepufferte Werte überleben das close

Eine zentrale Eigenschaft, die oft missverstanden wird: close verwirft nicht die bereits gepufferten Werte. Ein gepufferter Channel liefert seinen Inhalt vollständig aus, selbst wenn er geschlossen ist. Erst wenn der Buffer leer und der Channel geschlossen ist, gibt Recv den Zero-Value mit ok=false zurück.

Go buffer-close.go
package main

import "fmt"

func main() {
    ch := make(chan string, 5)
    ch <- "a"
    ch <- "b"
    ch <- "c"
    close(ch)

    // range liest die drei Werte, dann terminiert.
    for v := range ch {
        fmt.Println("got:", v)
    }
    fmt.Println("Schleife fertig")

    // Auch nach Schleifenende:
    v, ok := <-ch
    fmt.Printf("danach: v=%q ok=%v\n", v, ok)
}
Output
got: a
got: b
got: c
Schleife fertig
danach: v="" ok=false

Das ist praktisch wichtig: Du darfst einen gepufferten Channel füllen, schließen und an einen Konsumenten weiterreichen — er kriegt alle Werte. Damit lassen sich kleine „Snapshot"-Channels bauen, ohne dass Producer und Consumer parallel laufen müssen. Die Semantik bricht erst, wenn der Empfänger weniger erwartet als gesendet wurde — der Channel hält die Werte fest, aber niemand holt sie ab, und der Speicher wird erst beim GC wieder frei.

Muss man immer schließen?

Nein. close ist kein Cleanup-Pflichtaufruf wie file.Close(). Ein nicht geschlossener Channel ist kein Resource-Leak — der Garbage Collector räumt ihn auf, sobald alle Referenzen weg sind. Du brauchst close nur in zwei Fällen:

  • Der Empfänger nutzt range-Schleife oder comma-ok, und muss erkennen, dass das Ende erreicht ist.
  • Du nutzt close aktiv als Broadcast-Signal an mehrere Empfänger (z. B. ein done-Channel im Cancellation-Pattern).

In allen anderen Fällen darfst du den Channel ungeschlossen lassen. Klassisches Beispiel: ein langlebiger Worker-Channel, in den irgendwann nichts mehr gesendet wird — wenn keine Goroutine mehr lesen will, lässt der GC alles los, und das Programm geht ohne close sauber zu Ende.

Go kein-close-noetig.go
package main

import "fmt"

// Hier brauchen wir kein close — der Empfänger weiß,
// wie viele Werte er erwartet, und liest exakt so viele.
func main() {
    results := make(chan int, 3)

    for i := 0; i < 3; i++ {
        go func(n int) { results <- n * n }(i + 1)
    }

    for i := 0; i < 3; i++ {
        fmt.Println(<-results) // genau 3 Recvs, kein range
    }
    // results wird nicht geschlossen — vollkommen OK.
}

Der Daumenwert: Wenn dein Konsument-Code mit for ... := range ch arbeitet, muss irgendjemand schließen. Wenn dein Konsument eine feste Anzahl Werte erwartet (Counted Recv), darfst du schließen, musst aber nicht. Wenn ein Channel über das Programmende hinaus „leben" soll (Worker-Pool ohne Shutdown), schließe ihn nicht — das kostet nur potenzielle Panics.

Multi-Sender-Closing — das harte Problem

Die Producer-Convention wird kompliziert, sobald mehrere Goroutines auf denselben Channel senden. Wer schließt jetzt? Wenn jede Sender-Goroutine am Ende close aufruft, paniciert die zweite. Wenn keine schließt, hängt der range-Konsument.

Die Antwort aus dem Pipelines-Blog ist die WaitGroup-plus-Schließer-Goroutine: Eine zusätzliche Goroutine wartet auf alle Sender, und schließt eine einzige Mal, wenn alle fertig sind.

Go multi-sender-waitgroup.go
package main

import (
    "fmt"
    "sort"
    "sync"
)

// Fan-In: mehrere Input-Channels in einen Output mergen.
func merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    // Pro Input-Channel eine Sender-Goroutine.
    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            out <- v
        }
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }

    // EINE Schließer-Goroutine — wartet auf alle Sender,
    // dann genau einmal close(out).
    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func gen(vs ...int) <-chan int {
    out := make(chan int, len(vs))
    for _, v := range vs {
        out <- v
    }
    close(out)
    return out
}

func main() {
    a := gen(1, 2, 3)
    b := gen(10, 20, 30)
    c := gen(100, 200)

    var got []int
    for v := range merge(a, b, c) {
        got = append(got, v)
    }
    sort.Ints(got)
    fmt.Println(got)
}
Output
[1 2 3 10 20 30 100 200]

Das Pattern ist sauber, weil genau eine Goroutine close(out) aufruft, und sie tut es erst, nachdem die WaitGroup signalisiert, dass alle Sender ihren Loop verlassen haben. Damit ist Panic 1 (double close) ausgeschlossen, und Panic 2 (send on closed) auch, weil die Sender bei wg.Done() ihren Send-Loop bereits verlassen haben.

Das Alternativ-Pattern mit sync.Once ist kürzer, aber semantisch schwächer:

Go multi-sender-once.go
type SafeCloser struct {
    once sync.Once
    ch   chan int
}

func (s *SafeCloser) Close() {
    s.once.Do(func() { close(s.ch) })
}

sync.Once macht den Close idempotent, schützt also gegen Panic 1. Es schützt aber nicht gegen Panic 2 — ein paralleler Sender, der nach dem Close noch ein ch <- v versucht, paniciert weiterhin. Deshalb ist die WaitGroup-Variante in echten Pipelines der robustere Default.

done-Channel — Schließen als Broadcast

Es gibt eine zweite Verwendung von close, die nichts mit „Werte sind alle" zu tun hat: das Broadcast-Signal. Da Recv auf einem geschlossenen Channel sofort den Zero-Value liefert, kann ein einzelner close-Aufruf beliebig viele Goroutines gleichzeitig „aufwecken". Das ist das Fundament des Cancellation-Patterns aus dem Pipelines-Blog.

Go done-broadcast.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, done <-chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-done:
            fmt.Printf("worker %d: stop\n", id)
            return
        default:
            // Tu Arbeit ...
            time.Sleep(20 * time.Millisecond)
        }
    }
}

func main() {
    done := make(chan struct{})
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, done, &wg)
    }

    time.Sleep(50 * time.Millisecond)
    close(done) // EIN close — alle drei Worker sehen es
    wg.Wait()
    fmt.Println("alle Worker beendet")
}
Output
worker 1: stop
worker 3: stop
worker 2: stop
alle Worker beendet

Beachte den Typ: chan struct{}. Du sendest auf diesem Channel niemals einen Wert — das close allein ist das Signal. struct{} belegt null Byte; es ist der idiomatische „ich brauche nur das Ereignis"-Channel-Typ in Go. In modernem Code löst context.Context mit ctx.Done() dasselbe Problem standardisiert, aber unter der Haube ist es exakt dieser Mechanismus: ein Channel, dessen close als Broadcast wirkt.

Praxis 1 — Producer/Konsument mit range

Ein realistisches Pipeline-Setup: Ein Producer liest Zeilen aus einer Quelle, ein Konsument verarbeitet sie. Die Quelle terminiert irgendwann, der Konsument soll danach sauber beenden.

Go praxis-producer-consumer.go
package main

import (
    "fmt"
    "strings"
)

// Producer: simuliert eine Datenquelle (hier: Zeilen eines Strings).
// Er ist Besitzer von out und schließt ihn am Ende.
func readLines(text string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for _, line := range strings.Split(text, "\n") {
            if line == "" {
                continue
            }
            out <- line
        }
    }()
    return out
}

// Stage: jede Zeile großschreiben.
func upper(in <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for s := range in {
            out <- strings.ToUpper(s)
        }
    }()
    return out
}

func main() {
    input := "alice\nbob\ncarol\n"
    for s := range upper(readLines(input)) {
        fmt.Println(s)
    }
    fmt.Println("Pipeline fertig")
}
Output
ALICE
BOB
CAROL
Pipeline fertig

Jede Stage besitzt ihren Output-Channel und schließt ihn per defer close. Sobald readLines durch ist, propagiert sich das Endsignal: uppers range in terminiert, dadurch verlässt die Goroutine ihren Loop, der defer close(out) feuert, und die main-Schleife terminiert ebenfalls. Drei Goroutines, drei close-Aufrufe, kein einziger Lifecycle-Bug — das ist die Idiomatik im Reinformat.

Praxis 2 — Worker-Pool mit done-Cancellation

Ein zweiter klassischer Fall: Ein Pool aus Workern verarbeitet Jobs aus einem Input-Channel, schreibt Ergebnisse in einen Output-Channel, und alles muss bei einem externen Abbruch-Signal sauber stoppen.

Go praxis-workerpool.go
package main

import (
    "fmt"
    "sort"
    "sync"
)

func produceJobs(done <-chan struct{}) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := 1; i <= 8; i++ {
            select {
            case out <- i:
            case <-done:
                return // Abbruch — defer schließt out
            }
        }
    }()
    return out
}

func worker(id int, done <-chan struct{}, jobs <-chan int) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for j := range jobs {
            select {
            case out <- fmt.Sprintf("w%d:%d", id, j*j):
            case <-done:
                return
            }
        }
    }()
    return out
}

func merge(done <-chan struct{}, cs ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    out := make(chan string)
    send := func(c <-chan string) {
        defer wg.Done()
        for s := range c {
            select {
            case out <- s:
            case <-done:
                return
            }
        }
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go send(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {
    done := make(chan struct{})
    defer close(done) // Cleanup bei jedem Return-Pfad

    jobs := produceJobs(done)
    w1 := worker(1, done, jobs)
    w2 := worker(2, done, jobs)
    w3 := worker(3, done, jobs)

    var results []string
    for r := range merge(done, w1, w2, w3) {
        results = append(results, r)
    }
    sort.Strings(results)
    for _, r := range results {
        fmt.Println(r)
    }
}
Output
w1:1
w1:16
w1:49
w2:25
w2:4
w3:36
w3:64
w3:9

Das Beispiel zeigt drei verschiedene close-Rollen nebeneinander: produceJobs schließt jobs, wenn die Quelle erschöpft ist (Endsignal). Jeder worker schließt seinen eigenen Output-Channel, wenn sein Input durch ist (Endsignal). merge schließt den gemergten Output mit dem WaitGroup-Pattern (Multi-Sender-Close). Und done wird per defer close(done) in main geschlossen (Broadcast-Cancellation). Vier verschiedene Verwendungen von close, alle aus derselben Spec-Mechanik.

Zusammenfassende Lifecycle-Matrix

AktionErgebnis bei offenem ChannelErgebnis bei geschlossenem ChannelErgebnis bei nil-Channel
ch <- v (Send)blockiert/sendetPanic (send on closed channel)blockiert für immer
<-ch (Recv, gepuffert/non-empty)liefert Wertliefert Wertblockiert für immer
<-ch (Recv, leer)blockiertliefert Zero-Value sofortblockiert für immer
v, ok := <-ch (leer)blockiertv=zero, ok=falseblockiert für immer
for v := range chiteriertiteriert Restwerte, dann Endeblockiert für immer
close(ch)markiert geschlossenPanic (close of closed channel)Panic (close of nil channel)
len(ch), cap(ch)normale Wertenormale Werte (Buffer-Reste)0, 0

Die Tabelle macht die zwei Panic-Klassen sichtbar und zeigt, warum for v := range ch so robust ist: Es ist die einzige Konstruktion, die in jedem der drei Channel-Zustände eine sinnvolle Semantik hat (außer bei nil — da blockiert sie, was im select mit anderen Cases ein Feature ist).

Häufige Stolperfallen

Empfänger schließt — die kapitale Lifecycle-Sünde.

Wenn der Empfänger close(ch) aufruft, kann der Sender beim nächsten ch <- v mit send on closed channel panicen. Die Regel ist hart: Wer sendet, schließt. Wer empfängt, schließt nie. Bei Receive-Only-Typen <-chan T setzt der Compiler diese Regel statisch durch — nutze diesen Typ überall in API-Signaturen, wo der Aufrufer nur lesen soll.

Mehrere Sender, einer schließt — die zweite Panic.

Bei Multi-Sender-Patterns endet jedes naive defer close(out) in einer der Sender-Goroutines mit einer Panic, sobald eine andere Sender-Goroutine danach noch sendet. Lösung: separate Schließer-Goroutine plus sync.WaitGroup. Erst wenn wg.Wait() zurückkehrt, ist garantiert kein Sender mehr aktiv, und der einzige close(out) ist sicher.

`close` auf `nil`-Channel paniciert.

Vergessenes make führt zum Zero-Value des Channel-Typs — und der ist nil. close(ch) auf einem nil-Channel ist eine Runtime-Panic mit close of nil channel. Faustregel: Initialisiere Channels immer am selben Ort, an dem du den Producer-Goroutine startest.

Vergessenes `close` ⇒ `range` hängt für immer.

Wenn der Producer keinen defer close(out) setzt, blockiert for v := range out im Konsumenten unendlich, sobald die letzte Send-Operation vorbei ist. Das Programm friert ein, ohne sichtbare Fehler — der Klassiker im ersten Pipelines-Code. Cheatsheet: Jede Goroutine, die auf einem Channel sendet, beginnt mit defer close(out) direkt nach dem go func().

Send-Block bei `done`-Cancellation — vergessenes `select`.

Im Cancellation-Pattern muss jeder Send mit dem done-Channel in ein select gewickelt sein. Ein nacktes out <- v blockiert, wenn der nachgelagerte Konsument schon weggegangen ist (z. B. wegen Fehlers) — und die Producer-Goroutine bleibt für immer hängen. Das ist ein klassisches Goroutine-Leak, das in Tests nicht auffällt, aber unter Last sichtbar wird.

`close` ist kein Reset.

Ein geschlossener Channel kann nicht „wieder geöffnet" werden — es gibt keine reopen-Funktion. Wer einen Channel braucht, der mehrere „Generationen" durchläuft, erzeugt jedes Mal einen neuen mit make. Das ist auch der idiomatische Weg für „Phasen-Wechsel" in Pipelines, statt Tricks mit Pointer-zu-Channel oder dynamischem Reslicing.

`ok=false` heißt nicht „leer", sondern „geschlossen und leer".

Beim v, ok := <-ch ist ok=false nur dann, wenn der Channel geschlossen wurde und sein Buffer leer ist. Solange Werte im Buffer stehen, ist ok=true, auch wenn close bereits aufgerufen wurde. Das macht das Pattern robust gegen die Reihenfolge von Send/Close — gepufferte Werte gehen niemals verloren, nur weil der Producer schon mit close fertig ist.

`chan struct{}` für reine Signale, nicht `chan bool`.

Wenn ein Channel nur als Broadcast-Signal dient (typisch done), nutze chan struct{} — null Byte pro Element, keine Möglichkeit, „falsche Werte" zu senden. chan bool lädt zur Verwirrung ein: Was bedeutet done <- false? Das idiomatische Pattern signalisiert ausschließlich per close(done), nicht per Send. struct{} macht das mit dem Typsystem klar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Goroutines & Channels

Zur Übersicht