Channels Complete
Channels Complete
Overview
Channels are multi-purpose: data transfer, completion (done), and (with care) cancellation. This chapter covers close semantics, range, directional types, deadlocks, and nil-channel tricks.
End-of-stream: why close exists
Infinite receive loops deadlock when the producer finishes:
producer sends all words ──► consumer for { recv }
producer exits
consumer blocks forever on recv → "all goroutines are asleep - deadlock"
Magic sentinel values ("__EOF__") are fragile when the sentinel is also valid data. Close is the structured signal.
Closing a channel
// writer
for _, w := range words {
in <- w
}
close(in)
// reader
for {
w, ok := <-in
if !ok {
break // closed and drained
}
// use w
}| Operation | Open channel | Closed channel |
|---|---|---|
| Send | may block | panic |
| Receive | value, ok=true | zero value, ok=false (repeatable) |
| Close | ok once | panic on second close |
state machine:
start --> Open
Open --> Closed
Closed --> Closed
Open --> Open
Rules
- Only the writer closes (sole ownership).
- Close is a signal, not a resource free—GC collects unused channels either way.
- Close when readers need end-of-data; otherwise optional.
Range over channel
for w := range in {
// until closed
}Unlike slice range, channel range yields one value (not index).
Directional channels
Catch misuse at compile time:
func submit(str string, stream chan<- string) { /* send + close */ }
func receive(stream <-chan string) { /* range only */ }
stream := make(chan string) // bidirectional at creation
go submit(str, stream) // converts to send-only param
receive(stream) // converts to recv-only param| Type | Allowed |
|---|---|
chan T |
send, recv, close |
chan<- T |
send, close |
<-chan T |
recv only |
Done channel
Wait for work without a WaitGroup:
func say(done chan<- struct{}, id int, phrase string) {
// ... work ...
done <- struct{}{} // or close(done) for multi-waiter broadcast
}
done := make(chan struct{})
go say(done, 1, "hello")
<-donedone: worker ──► waiter "finished"
struct{} uses zero bytes of payload—signal only.
Deadlocks
Go runtime detects when all goroutines are blocked:
fatal error: all goroutines are asleep - deadlock!
Common causes:
- Unbuffered send without receiver
- Receive without sender
- WaitGroup counter never reaches zero
- Mutual wait (A waits B, B waits A)
Nil channels
var ch chan int // nil
// <-ch blocks forever
// ch <- 1 blocks foreverUseful in select: set a case’s channel to nil to disable that case after it closes (see pipelines merge).
Buffered channels
ch := make(chan int, 3) // send up to 3 without a receivercapacity 3:
send s1 s2 s3 → buffer full → 4th send blocks
recv frees a slot
Buffer size is a capacity / decoupling choice, not free async magic.
Runnable example
go mod init example && go run .package main
import "fmt"
func main() {
in := make(chan string)
go func() {
for _, w := range []string{"one", "two", "", "four"} {
in <- w
}
close(in)
}()
for w := range in {
if w != "" {
fmt.Print(w, " ")
}
}
fmt.Println()
// closed receives
v, ok := <-in
fmt.Printf("after close: %q ok=%v\n", v, ok)
}Expected:
one two four
after close: "" ok=false
Try next: Make receive take <-chan string and try to close inside it—confirm compile error.