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 functionclose(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.
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
}1
2
3
0Die 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:
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)
}
}empfangen: 0
empfangen: 42
Channel geschlossen, fertigOhne 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:
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")
}0
1
2
3
4
Schleife terminiert sauberDas 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
closeaufzurufen. - 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 <- vplötzlich nicht, dass der Channel zu ist, und das Programm pansicht.
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
}Summe: 10Beachte 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.
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.
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
nilchannel 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.
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
}Timeout — nilCh war nie bereitIn 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.
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)
}got: a
got: b
got: c
Schleife fertig
danach: v="" ok=falseDas 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 odercomma-ok, und muss erkennen, dass das Ende erreicht ist. - Du nutzt
closeaktiv als Broadcast-Signal an mehrere Empfänger (z. B. eindone-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.
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.
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)
}[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:
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.
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")
}worker 1: stop
worker 3: stop
worker 2: stop
alle Worker beendetBeachte 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.
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")
}ALICE
BOB
CAROL
Pipeline fertigJede 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.
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)
}
}w1:1
w1:16
w1:49
w2:25
w2:4
w3:36
w3:64
w3:9Das 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
| Aktion | Ergebnis bei offenem Channel | Ergebnis bei geschlossenem Channel | Ergebnis bei nil-Channel |
|---|---|---|---|
ch <- v (Send) | blockiert/sendet | Panic (send on closed channel) | blockiert für immer |
<-ch (Recv, gepuffert/non-empty) | liefert Wert | liefert Wert | blockiert für immer |
<-ch (Recv, leer) | blockiert | liefert Zero-Value sofort | blockiert für immer |
v, ok := <-ch (leer) | blockiert | v=zero, ok=false | blockiert für immer |
for v := range ch | iteriert | iteriert Restwerte, dann Ende | blockiert für immer |
close(ch) | markiert geschlossen | Panic (close of closed channel) | Panic (close of nil channel) |
len(ch), cap(ch) | normale Werte | normale 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
- Close — Go Language Specification
- Receive operator — Go Specification (comma-ok-Form)
- Send statements — Go Specification
- For statements with range clause — Go Specification
- Go Concurrency Patterns: Pipelines and Cancellation (Go Blog)
- Effective Go: Channels
sync.WaitGroupPackage Documentationsync.OncePackage Documentation