Das Keyword chan deklariert einen Channel-Typ. Ein Channel ist eine typisierte Leitung, über die Goroutines einander Werte übergeben. Jedes Senden und Empfangen synchronisiert dabei auch die beiden beteiligten Goroutines. chan Job transportiert Job-Werte in beide Richtungen, chan<- Job kann nur senden und <-chan Job nur empfangen. Ein Channel muss vor der Verwendung mit make erstellt werden, denn der Nullwert eines Channel-Typs ist nil (Go spec).
Kurzfassung
make(chan Job)erstellt einen ungepufferten Channel. Ein Senden blockiert, bis eine andere Goroutine empfängt. Jedes Senden ist also eine Übergabe.make(chan Job, 100)erstellt einen gepufferten Channel. Senden blockiert nur, wenn der Buffer voll ist, und Empfangen nur, wenn er leer ist.chan<- Jobund<-chan Jobin einer Funktionssignatur beschränken die Funktion aufs Senden oder Empfangen. Alles andere lehnt der Compiler ab.- Der Sender schließt den Channel, niemals der Empfänger.
for job := range jobsendet nach einem Close, undv, ok := <-chliefertok == false, sobald der Channel geschlossen und leer ist. - Senden auf einen geschlossenen Channel löst eine Panic aus, doppeltes Schließen ebenfalls. Jede Operation auf einem
nil-Channel außercloseblockiert für immer. fatal error: all goroutines are asleep - deadlock!heißt, dass jede Goroutine blockiert ist. Ein Leak ist die leisere Variante, bei der nur einige Goroutines festhängen. Go 1.27 findet sie mit dem Profilgoroutineleak.selectwartet gleichzeitig auf mehrere Channel-Operationen. Damit bekommst du Timeouts, Abbruch und nicht-blockierendes Senden.
Wie deklariert und erstellt man einen Channel in Go?
Schreib chan, gefolgt vom Elementtyp. Eine so deklarierte Variable ist nil, bis du ihr einen Channel aus make zuweist:
example.gogopackage main import "fmt" type Job struct { ID int URL string } func main() { var queue chan Job fmt.Println(queue == nil, len(queue), cap(queue)) queue = make(chan Job, 100) queue <- Job{ID: 1, URL: "https://api.example.com/webhooks/stripe"} queue <- Job{ID: 2, URL: "https://api.example.com/webhooks/github"} fmt.Println(len(queue), cap(queue)) job := <-queue fmt.Println(job.ID, job.URL) }
example.texttexttrue 0 0 2 100 1 https://api.example.com/webhooks/stripe
ch <- v sendet und <-ch empfängt. Der Pfeil zeigt immer in die Richtung, in die sich die Daten bewegen. cap liefert die Buffergröße, die du an make übergeben hast, und len die Anzahl der Werte, die gerade im Buffer liegen. Die Werte kommen in der Reihenfolge heraus, in der sie hineingegangen sind.
Ein Channel ist eine Referenz auf eine Struktur der Runtime, genau wie eine Map. Übergibst du ihn an eine Funktion oder speicherst ihn in einem Struct, wird die Referenz kopiert. Jede Kopie spricht also mit demselben Channel. Das Go-Keyword var und var vs. make behandeln Channels, Maps und Slices als die drei Typen, die du normalerweise mit make erstellst.
Was ist der Unterschied zwischen gepufferten und ungepufferten Channels?
Ein ungepufferter Channel hat keinen Speicher. Ein Senden wartet, bis ein Empfänger den Wert abnimmt, und ein Empfangen wartet, bis ein Sender einen anbietet. Beide Seiten treffen sich im selben Moment. Die Go-Spezifikation formuliert es so: „communication succeeds only when both a sender and receiver are ready“. Die Kommunikation gelingt also nur, wenn Sender und Empfänger beide bereit sind.
Hier übergibt ein Upload-Handler einen Dateinamen an einen Virenscanner, der 100ms zum Starten braucht:
example.gogofunc main() { uploads := make(chan string) go func() { time.Sleep(100 * time.Millisecond) fmt.Println("scanner: ready") for name := range uploads { _ = name // virus-scan the file } }() start := time.Now() uploads <- "invoice-1001.pdf" fmt.Println("handler: send returned after", time.Since(start).Round(10*time.Millisecond)) }
example.texttextscanner: ready handler: send returned after 100ms
Das Senden konnte erst abgeschlossen werden, als der Scanner zum Empfangen bereit war. Wenn das Senden zurückkehrt, weiß der Handler, dass der Scanner den Wert hat. Diese Garantie ist für sich schon nützlich, und das Go Memory Model hält sie formal fest: Ein Empfangen aus einem ungepufferten Channel ist synchronisiert, bevor das zugehörige Senden abgeschlossen ist.
Ein gepufferter Channel speichert bis zu cap Werte. Mit make(chan string, 10) wäre das Senden des Handlers sofort zurückgekehrt, und der Scanner hätte die Datei 100ms später abgeholt. Der Sender blockiert erst, wenn alle 10 Plätze belegt sind.
Wie groß sollte der Buffer eines Go-Channels sein?
Fang mit null oder eins an und wähl eine größere Zahl nur, wenn du sagen kannst, wofür sie da ist. Ein Buffer macht einen langsamen Consumer nicht schneller. Sind die Producer dauerhaft schneller als die Consumer, läuft ein Buffer von 1.000 voll und verhält sich danach wie ein ungepufferter Channel, nur mit 1.000 Werten zusätzlicher Latenz davor.
Buffer helfen in drei Situationen:
- Eine Goroutine, die ein einzelnes Ergebnis liefert. Mit
make(chan Result, 1)kann sie senden und sich beenden, auch wenn niemand empfängt. Genau das behebt den Leak, der weiter unten gezeigt wird. - Lastspitzen. Ein Metrics-Exporter, der 50 Samples auf einmal bekommt und sie einmal pro Sekunde gebündelt verschickt, kann einen Buffer in der Größe einer normalen Spitze verwenden.
- Begrenzte Nebenläufigkeit. Ein gepufferter Channel mit Kapazität N dient als Semaphore, die N Goroutines gleichzeitig laufen lässt. Ein Beispiel steht im Abschnitt über Patterns.
Was bedeuten chan<- und <-chan in Go?
Das sind gerichtete Channel-Typen. chan<- Job ist send-only und <-chan Job ist receive-only. Ein bidirektionales chan Job wird automatisch in jeden der beiden umgewandelt. Du erstellst den Channel also einmal und gibst jeder Funktion nur die Richtung, die sie braucht.
Eine Webhook-Pipeline mit einem Producer und einer Zustellstufe sieht so aus:
example.gogotype Result struct { JobID int Status int } func produce(ids []int, out chan<- Job) { for _, id := range ids { out <- Job{ID: id, URL: fmt.Sprintf("https://hooks.example.com/%d", id)} } close(out) } func deliver(in <-chan Job, results chan<- Result) { for job := range in { results <- Result{JobID: job.ID, Status: 200} } close(results) } func main() { jobs := make(chan Job) results := make(chan Result) go produce([]int{101, 102, 103}, jobs) go deliver(jobs, results) for r := range results { fmt.Printf("job %d: %d\n", r.JobID, r.Status) } }
example.texttextjob 101: 200 job 102: 200 job 103: 200
Die Signaturen dokumentieren, wem welches Ende gehört. Wer deliver liest, weiß, dass die Funktion nie in in schreibt und in nie schließt. Der Compiler setzt das durch. Versucht deliver, auf seinen Eingang zu senden oder ihn zu schließen:
example.gogopackage main type Job struct{ ID int } func deliver(in <-chan Job) { in <- Job{ID: 1} close(in) }
example.texttext./main.go:6:2: invalid operation: cannot send to receive-only channel <-chan Job in (variable of type <-chan Job) ./main.go:7:8: invalid operation: cannot close receive-only channel in (variable of type <-chan Job)
Schließen zählt als Operation der Sendeseite, deshalb lässt sich nur ein chan<- oder ein bidirektionaler Channel schließen. Die übliche Ownership-Regel „der Sender schließt“ prüft damit das Typsystem für dich.
Wie schließt man einen Channel in Go?
Mit close(ch). Das Schließen teilt den Empfängern mit, dass keine weiteren Werte mehr kommen. Werte, die schon im Buffer liegen, verwirft es nicht. Die Empfänger bekommen zuerst diese, und danach kehrt jedes Empfangen sofort mit dem Nullwert zurück:
example.gogofunc main() { events := make(chan string, 2) events <- "user.created" events <- "user.deleted" close(events) for i := 0; i < 3; i++ { ev, ok := <-events fmt.Printf("%q %v\n", ev, ok) } }
example.texttext"user.created" true "user.deleted" true "" false
Der zweite Wert, ok, ist true für einen echten Wert und false, sobald der Channel geschlossen und leer ist. Ein einfaches ev := <-events kann einen gesendeten leeren String nicht vom Nullwert eines geschlossenen Channels unterscheiden. Verwende deshalb die Form mit zwei Werten, wann immer der Channel geschlossen sein könnte.
for v := range ch macht dieselbe Prüfung für dich. Die Schleife empfängt, bis der Channel geschlossen und leer ist, und endet dann. So weiß deliver oben, wann Schluss ist, und deshalb muss produce den Channel jobs schließen, wenn ihm die IDs ausgehen. Ohne das Close würde deliver für immer auf einen vierten Job warten.
Wer sollte einen Go-Channel schließen?
Die Goroutine, die sendet. Der Empfänger kann nicht wissen, ob noch ein Wert unterwegs ist, und ein Schließen von der Empfangsseite führt direkt zu den Panics weiter unten. Senden mehrere Goroutines auf einen Channel, kann keine von ihnen ihn allein schließen. Eine separate Goroutine wartet dann mit einer sync.WaitGroup auf alle und schließt den Channel danach. Der Worker-Pool weiter unten zeigt dieses Muster.
Drei Fehler lösen zur Laufzeit eine Panic aus:
example.texttextpanic: send on closed channel panic: close of closed channel panic: close of nil channel
Du musst nicht jeden Channel schließen. Ein Channel, auf den nichts mehr verweist, wird vom Garbage Collector eingesammelt, egal ob er geschlossen wurde oder nicht. Schließ einen Channel, wenn Empfänger wissen müssen, dass der Stream zu Ende ist, etwa bei einer range-Schleife oder einem Shutdown-Signal.
Was passiert beim Senden auf einen nil- oder geschlossenen Channel?
Hier sind alle Kombinationen auf einen Blick:
| Operation | nil-Channel | Offener Channel | Geschlossener Channel |
|---|---|---|---|
Senden ch <- v | Blockiert für immer | Blockiert, bis ein Empfänger den Wert abnimmt oder im Buffer Platz frei wird | Panic |
Empfangen <-ch | Blockiert für immer | Liefert einen gepufferten Wert oder blockiert, bis einer ankommt | Liefert gepufferte Werte, danach den Nullwert mit ok == false |
close(ch) | Panic | Klappt | Panic |
Ein select-Case auf einem nil-Channel wird nie ausgewählt. Darauf baut das Merge-Beispiel weiter unten auf. Ein geschlossener Channel kehrt für jeden Empfänger sofort zurück, und dadurch funktioniert close als Broadcast. Jede Goroutine, die aus dem Channel empfängt, wacht zur selben Zeit auf.
Warum meldet Go „all goroutines are asleep - deadlock!“?
Weil die Runtime festgestellt hat, dass jede Goroutine im Programm blockiert ist und keine von ihnen je wieder aufwachen kann. Die kleinste Variante ist ein Senden auf einen ungepufferten Channel ohne Empfänger:
example.gogofunc main() { uploads := make(chan string) uploads <- "invoice-1001.pdf" fmt.Println("queued") }
example.texttextfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /app/main.go:7 +0x34
main ist die einzige Goroutine, und sie wartet auf ein Senden, das niemand empfangen wird. Der Trace nennt die blockierte Operation (chan send) und die Zeile. Die Lösung ist, in einer anderen Goroutine zu empfangen oder dem Channel einen Buffer zu geben, wenn du nur einen einzigen Wert ablegen musst. Häufige Fehler in Go führt das als zehnten Fehler auf.
Der Detektor schlägt nur an, wenn jede Goroutine festhängt. Ein Webserver hat immer eine Goroutine, die im Network Poller auf die nächste Verbindung wartet, und die zählt der Detektor nicht als festhängend. Ein Handler, der auf einem Channel blockiert, löst ihn also nie aus. Der Handler bleibt einfach stehen, und seine Goroutine bleibt im Speicher, bis der Prozess endet. Das nennt man einen Goroutine-Leak.
Wie findet man Goroutine-Leaks in Go?
Ein typischer Leak sieht so aus. Eine Checkout-Seite fragt drei Versanddienstleister nach einem Angebot und nimmt die schnellste Antwort:
example.gogofunc fastestQuote(ctx context.Context) (Quote, error) { ch := make(chan Quote) go func() { ch <- fetchQuote("ups", 50*time.Millisecond) }() go func() { ch <- fetchQuote("dhl", 80*time.Millisecond) }() go func() { ch <- fetchQuote("fedex", 300*time.Millisecond) }() select { case q := <-ch: return q, nil case <-ctx.Done(): return Quote{}, ctx.Err() } }
Die Funktion empfängt einmal und kehrt zurück. Die beiden langsameren Goroutines versuchen danach, auf einen ungepufferten Channel zu senden, den nie jemand liest. Sie bleiben blockiert, solange der Prozess läuft. Nach 100 Checkouts hängen 200 Goroutines fest.
Go 1.27 hat das Profil goroutineleak allgemein verfügbar gemacht, nachdem es in Go 1.26 ein Release lang als Experiment dabei war (Go 1.27 Release Notes). Der Garbage Collector sucht nach Goroutines, die auf einem Channel oder Lock blockiert sind, der von keiner lauffähigen Goroutine mehr erreichbar ist, und meldet sie nach Stack:
example.gogopprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
example.texttextgoroutineleak profile: total 200 100 @ 0x1044f99a8 0x104492854 0x104492468 0x104550560 0x1044ffc74 # 0x10455055f main.fastestQuote.func3+0x4f /app/main.go:27 100 @ 0x1044f99a8 0x104492854 0x104492468 0x1045505d0 0x1044ffc74 # 0x1045505cf main.fastestQuote.func2+0x4f /app/main.go:26
Das Profil zeigt mit Zeilennummer auf die DHL- und die FedEx-Goroutine. Ein Server, der net/http/pprof importiert, stellt dieselben Daten unter /debug/pprof/goroutineleak bereit. Was ist neu in Go 1.27 behandelt das Profil ausführlicher.
Die Lösung ist ein Buffer mit Platz für jeden Sender:
example.gogoch := make(chan Quote, 3)
Jetzt kann jede Goroutine senden und sich beenden, egal ob jemand ihren Wert liest. Mit dieser Änderung meldet dasselbe Programm goroutineleak profile: total 0. Allgemein gilt, dass jede Goroutine, die du startest, irgendeinen Weg zum Ende braucht. Meist ist das ein Empfänger, der immer liest, ein Buffer, der groß genug für jedes Senden ist, oder ein select auf ctx.Done().
Wie funktioniert select mit Channels in Go?
select wartet auf mehrere Channel-Operationen und führt die erste aus, die weitermachen kann. Sind mehrere gleichzeitig bereit, wählt es zufällig eine aus, sodass kein Case die anderen aushungern kann (Go spec).
Wie versieht man das Empfangen aus einem Channel mit einem Timeout?
Pack das Empfangen und einen Timeout-Channel in dasselbe select. Mit einem context.Context ist der Timeout-Channel ctx.Done():
example.gogofunc lookupPrice(ctx context.Context, sku string) (int, error) { result := make(chan int, 1) go func() { result <- slowPriceService(sku) }() select { case cents := <-result: return cents, nil case <-ctx.Done(): return 0, fmt.Errorf("price for %s: %w", sku, ctx.Err()) } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() fmt.Println(lookupPrice(ctx, "SKU-1001")) fmt.Println(lookupPrice(context.Background(), "SKU-1001")) }
example.texttext0 price for SKU-1001: context deadline exceeded 4999 <nil>
slowPriceService braucht 300ms. Der erste Aufruf gibt deshalb nach 100ms auf, und der zweite wartet auf die Antwort. Der Buffer von 1 auf result ist die Leak-Lösung aus dem vorigen Abschnitt. Nach einem Timeout kann die Goroutine ihre verspätete Antwort trotzdem senden und sich beenden.
case <-time.After(2 * time.Second): funktioniert ebenfalls, wenn es keinen Context gibt. Seit Go 1.23 kann ein Timer, auf den nichts mehr verweist, vom Garbage Collector eingesammelt werden, bevor er feuert. time.After in einer Schleife stapelt also keine Timer mehr auf (Go 1.23 Release Notes). Go 1.27 hat die GODEBUG-Einstellung asynctimerchan entfernt, mit der sich das alte Verhalten zurückholen ließ. In Code, der Requests verarbeitet, nimm lieber den Context, denn er trägt auch den Abbruch durch den Client mit sich.
Wie sendet oder empfängt man, ohne zu blockieren?
Füg einen default-Case hinzu. select führt default aus, wenn kein anderer Case bereit ist, sodass die Operation nie wartet. Ein Metrics-Exporter, der den Request-Pfad niemals ausbremsen darf, verwirft damit Samples, wenn seine Queue voll ist:
example.gogotype Exporter struct { queue chan Metric dropped atomic.Int64 } func (e *Exporter) Record(m Metric) { select { case e.queue <- m: default: e.dropped.Add(1) } } func main() { e := &Exporter{queue: make(chan Metric, 3)} for i := range 5 { e.Record(Metric{Name: "http_requests_total", Value: float64(i)}) } fmt.Println("queued:", len(e.queue), "dropped:", e.dropped.Load()) }
example.texttextqueued: 3 dropped: 2
Record läuft in vielen Request-Handlern gleichzeitig, deshalb ist der Zähler für verworfene Samples ein atomic.Int64 und kein einfacher int. Zähl die verworfenen Samples mit, denn ein stilles default verbirgt den Moment, in dem der Consumer nicht mehr hinterherkommt.
Warum setzt man einen Channel in einem select auf nil?
Um einen Case abzuschalten. Ein Empfangen aus einem nil-Channel blockiert für immer, also kann ein select-Case auf einem nil-Channel nie ausgewählt werden. Wenn du zwei Streams zusammenführst, setz jeden auf nil, sobald er geschlossen ist, und hör auf, wenn beide es sind:
example.gogofunc merge(a, b <-chan string) []string { var out []string for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil continue } out = append(out, v) case v, ok := <-b: if !ok { b = nil continue } out = append(out, v) } } return out }
Ohne die Zuweisung von nil ist ein geschlossener Channel immer bereit, und die Schleife würde mit voller CPU-Last über Nullwerte rotieren.
Welche Channel-Patterns gibt es in Go?
Vier Patterns decken den Großteil des Channel-Codes in Produktionsservices ab.
Worker-Pool
Eine feste Anzahl von Goroutines liest aus einem gemeinsamen Job-Channel. Dieser Pool stellt Webhooks mit drei Workern zu:
example.gogofunc main() { hooks := make(chan Webhook) deliveries := make(chan Delivery) var wg sync.WaitGroup for worker := range 3 { wg.Go(func() { for h := range hooks { deliveries <- Delivery{WebhookID: h.ID, Worker: worker, Status: send(h)} } }) } go func() { for id := 1; id <= 9; id++ { hooks <- Webhook{ID: id, URL: fmt.Sprintf("https://partner.example.com/hooks/%d", id)} } close(hooks) }() go func() { wg.Wait() close(deliveries) }() ok := 0 for d := range deliveries { if d.Status == 200 { ok++ } } fmt.Println("delivered:", ok) }
example.texttextdelivered: 9
Damit der Pool korrekt arbeitet, kommt es auf vier Details an. Der Producer schließt hooks, damit die range-Schleife jedes Workers endet. wg.Go, seit Go 1.25 verfügbar, startet jeden Worker und registriert ihn in einem Aufruf. Eine separate Goroutine wartet auf alle Worker, bevor sie deliveries schließt, denn kein einzelner Worker weiß, wann die anderen fertig sind. Die Closures erfassen worker gefahrlos, seit Go 1.22 jeder Schleifeniteration eine eigene Variable gibt (siehe wie Closures funktionieren).
Fan-out und Fan-in
Der Worker-Pool ist Fan-out: Ein Channel versorgt viele Goroutines. Der Channel deliveries ist Fan-in: Viele Goroutines schreiben in einen Channel, den eine einzige Schleife liest. Die Funktion merge oben ist Fan-in für eine feste Anzahl von Eingangs-Channels. Der Pipelines-Artikel im Go-Blog baut aus denselben zwei Bausteinen längere Ketten.
Semaphore
Ein gepufferter Channel aus leeren Structs begrenzt, wie viele Goroutines etwas gleichzeitig tun. Hier ein Bildservice, der nicht mehr als drei Bilder parallel skalieren darf:
example.gogosem := make(chan struct{}, 3) var wg sync.WaitGroup for _, img := range images { wg.Go(func() { sem <- struct{}{} defer func() { <-sem }() resizeImage(img) }) } wg.Wait()
Die vierte Goroutine blockiert bei sem <- struct{}{}, bis eine der ersten drei ihren Platz freigibt. Das Memory Model garantiert das für einen Channel mit Kapazität C: Das k-te Empfangen geschieht, bevor das (k+C)-te Senden abgeschlossen ist (Go Memory Model). Wenn die Goroutines auch Fehler zurückgeben, erledigt errgroup.Group mit SetLimit aus golang.org/x/sync dieselbe Aufgabe und sammelt den ersten Fehler ein.
Done-Signal mit chan struct{}
Ein Channel, der nur geschlossen und nie zum Senden verwendet wird, sendet ein Signal an jeden Empfänger. struct{} ist der Elementtyp, weil er null Bytes belegt und klarmacht, dass keine Daten fließen (siehe das Go-Keyword struct):
example.gogofunc flushLoop(done <-chan struct{}, flushed chan<- int) { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() n := 0 for { select { case <-ticker.C: n++ case <-done: flushed <- n return } } } func main() { done := make(chan struct{}) flushed := make(chan int) go flushLoop(done, flushed) time.Sleep(175 * time.Millisecond) close(done) fmt.Println("flushes before shutdown:", <-flushed) }
example.texttextflushes before shutdown: 3
context.Context baut auf derselben Idee auf. ctx.Done() liefert einen <-chan struct{}, der beim Abbruch geschlossen wird. Er funktioniert deshalb in jedem select genauso wie done oben.
Channel oder Mutex: Was solltest du in Go verwenden?
Nimm einen Channel, wenn du die Ownership von Daten weitergibst oder Goroutines koordinierst. Nimm einen sync.Mutex, wenn mehrere Goroutines einen gemeinsamen Zustand direkt verändern. Das Go-Sprichwort „Don't communicate by sharing memory, share memory by communicating“ (Go Proverbs) beschreibt den ersten Fall, und die Go-Wiki-Seite Use a sync.Mutex or a channel? stellt klar, dass das Sprichwort Mutexe nicht ausschließt.
Ein Request-Zähler pro Route ist Zustand und kein Fluss von Werten:
example.gogotype Stats struct { mu sync.Mutex counts map[string]int } func (s *Stats) Inc(route string) { s.mu.Lock() defer s.mu.Unlock() s.counts[route]++ }
Die Channel-Variante braucht eine Goroutine, der die Map gehört, einen Request-Channel und einen Antwort-Channel für Lesezugriffe. Das ist mehr Code, und jedes Inkrementieren muss warten, bis eine andere Goroutine es bearbeitet. Bei einer Job-Queue ist es umgekehrt. Ein durch einen Mutex geschützter Slice braucht eine sync.Cond oder Polling, damit Worker auf Arbeit warten, während ein Channel sie ohne Zusatzaufwand blockiert und wieder aufweckt.
| Nimm einen Channel für | Nimm einen Mutex für |
|---|---|
| die Übergabe eines Jobs oder Ergebnisses an eine andere Goroutine | Zähler, Caches und Maps, die direkt verändert werden |
| Signale für Shutdown, Abbruch oder Fertigstellung | den Schutz einiger Felder in einem Struct |
| das Begrenzen von Nebenläufigkeit oder den Bau von Pipelines | kurze kritische Abschnitte auf Hot Paths |
Teste beide Varianten mit go test -race. Der Race Detector meldet eine Map, die ohne Lock geteilt wird. Er meldet auch einen Sender, der ein Struct weiter verändert, nachdem er einen Pointer darauf gesendet hat, während der Empfänger es liest.
Wo LevelUpGo ins Spiel kommt
LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Go Concurrency Fundamentals behandelt Goroutines, ungepufferte und gepufferte Channels, Channel-Richtungen, select, context und den Race Detector, mit Übungen, die in einen Deadlock laufen, bis du sie reparierst. Go Design Patterns baut die Patterns Worker-Pool, Fan-out und Fan-in sowie Graceful Shutdown aus diesem Artikel als vollständige Übungen auf. Im Training Ground findest du kurze, eigenständige Übungen zur Nebenläufigkeit mit Code, der in einen Deadlock läuft, Goroutines leakt oder Arbeit verliert. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.
Häufig gestellte Fragen
Ist chan ein Keyword in Go?
Ja. chan ist eines der 25 reservierten Keywords von Go, du kannst es also nicht als Variablen- oder Funktionsnamen verwenden. Es kommt in Channel-Typen wie chan Job, chan<- Job und <-chan Job vor. Der Operator <- ist kein Keyword, und make ebenso wenig, denn das ist eine vordeklarierte Funktion.
Muss man einen Channel in Go schließen?
Nein. Schließ einen Channel nur, wenn Empfänger wissen müssen, dass keine weiteren Werte mehr kommen, zum Beispiel um eine for range-Schleife zu beenden oder einen Shutdown zu signalisieren. Ein nicht geschlossener Channel, auf den nichts mehr verweist, wird wie jeder andere Wert vom Garbage Collector eingesammelt.
Was ist der Unterschied zwischen einem gepufferten und einem ungepufferten Channel?
Bei einem ungepufferten Channel, make(chan T), wartet der Sender, bis ein Empfänger den Wert abnimmt. Beide Goroutines treffen sich also. Bei einem gepufferten Channel, make(chan T, n), können bis zu n Werte im Channel warten, und der Sender blockiert erst, wenn der Buffer voll ist.
Was passiert, wenn man in Go aus einem geschlossenen Channel liest?
Zuerst bekommst du alle Werte, die noch im Buffer liegen. Danach kehrt jedes Empfangen sofort mit dem Nullwert des Elementtyps zurück. Die Form mit zwei Werten, v, ok := <-ch, setzt ok bei diesen Nullwerten auf false, sodass du sie von echten Werten unterscheiden kannst.
Wie prüft man in Go, ob ein Channel geschlossen ist?
Empfang mit v, ok := <-ch und prüf ok. Es gibt keine Funktion, die meldet, ob ein Channel geschlossen ist, ohne zu empfangen, und len(ch) zählt nur gepufferte Werte. Code, der so eine Prüfung braucht, hat meist mehr als eine Goroutine, die den Channel schließt, und genau dieses Design solltest du korrigieren.
Kann man in Go mit range über einen Channel iterieren?
Ja. for v := range ch empfängt Werte, bis der Channel geschlossen und leer ist, und verlässt dann die Schleife. Schließt niemand den Channel, wartet die Schleife für immer. Der Sender muss also close aufrufen, wenn er fertig ist.
Quellen
- The Go Programming Language Specification, Channel types: https://go.dev/ref/spec#Channel_types
- The Go Programming Language Specification, Send statements: https://go.dev/ref/spec#Send_statements
- The Go Programming Language Specification, Receive operator: https://go.dev/ref/spec#Receive_operator
- The Go Programming Language Specification, Close: https://go.dev/ref/spec#Close
- The Go Programming Language Specification, Select statements: https://go.dev/ref/spec#Select_statements
- The Go Memory Model, Channel communication: https://go.dev/ref/mem#chan
- Effective Go, Concurrency: https://go.dev/doc/effective_go#concurrency
- The Go Blog, Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines
- Go Wiki, Use a sync.Mutex or a channel?: https://go.dev/wiki/MutexOrChannel
- Go 1.23 Release Notes, Timer changes: https://go.dev/doc/go1.23#timer-changes
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
- Go Proverbs: https://go-proverbs.github.io/
