Closing and Ranging Over Channels in Go
Why a range loop over a channel never stops until the channel is closed, who should call close, and the panics caused by closing wrongly.
One of the most common Go questions is "why does my for range over a channel never stop?" The answer is that a range loop over a channel keeps waiting for values until the channel is closed. Closing is how the sender says "there is nothing more coming".
A Range Loop That Ends
package main
import "fmt"
func main() {
nums := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
nums <- i
}
close(nums)
}()
for n := range nums {
fmt.Println(n)
}
fmt.Println("channel closed, loop finished")
}
Output:
1
2
3
channel closed, loop finished
close(nums) marks the channel as finished. The loop receives the three values that were sent, notices the channel is closed and empty, and exits.
What Happens Without close
Remove close(nums) and run it again. The loop prints 1, 2, 3 and then waits for a fourth value forever. The sending goroutine has already finished, and nothing else can send, so Go detects that every goroutine is stuck:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
The [chan receive] part tells you what the stuck goroutine was doing: waiting to receive from a channel.
In a server, there is no crash:
The deadlock message only appears when every goroutine is stuck. In a web server, other goroutines are still running, so a forgotten close does not crash anything. The stuck goroutine just waits forever and leaks memory. This makes the bug harder to notice than in small programs.
Who Should Close a Channel
The rule is simple: the sender closes, never the receiver. Only the sender knows when there is no more data. If the receiver closes and the sender then sends, the program crashes:
panic: send on closed channel
Closing twice also crashes:
panic: close of closed channel
When several goroutines send on one channel, none of them can close it alone, because the others might still be sending. Use a WaitGroup and close after all senders are done:
package main
import (
"fmt"
"sync"
)
func main() {
results := make(chan string)
var wg sync.WaitGroup
for _, site := range []string{"a.com", "b.com", "c.com"} {
wg.Add(1)
go func() {
defer wg.Done()
results <- "checked " + site
}()
}
go func() {
wg.Wait()
close(results)
}()
count := 0
for r := range results {
fmt.Println(r)
count++
}
fmt.Println("total:", count)
}
The extra goroutine waits for all senders and then closes results. The range loop in main ends after the last value. This "wait then close in a separate goroutine" shape is one of the most used patterns in Go.
Do not wg.Wait() before the range loop:
Calling wg.Wait() in main and then ranging deadlocks: the senders are blocked trying to send on the unbuffered channel, because nobody is receiving yet, so Wait never returns. Waiting must happen in its own goroutine.
Receiving From a Closed Channel
A receive on a closed channel never blocks. It returns the zero value of the type right away. To tell a real zero from "closed", use the two-value form:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 0
close(ch)
v, ok := <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
}
Output:
0 true
0 false
The first 0 was really sent, so ok is true. The second comes from the closed, empty channel, so ok is false. Note that buffered values are still delivered after close.
Closing as a Broadcast Signal
Because every receive on a closed channel returns immediately, closing is a way to tell many goroutines at once to stop:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
done := make(chan struct{})
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-done
fmt.Println("worker", i, "stopping")
}()
}
time.Sleep(20 * time.Millisecond)
close(done)
wg.Wait()
}
All three workers print their stop message after close(done). A send would have woken only one of them. The context package uses this same idea under the hood for cancellation.
Closing done right:
The sender closes exactly once, after its last send, and receivers either range over the channel or check ok. With those habits, send on closed channel and forever-waiting loops do not happen.
Summary
for v := range ch keeps waiting until ch is closed, so a missing close causes a deadlock or a leaked goroutine. Only the sender closes, exactly once, and with several senders you close after a WaitGroup finishes in a separate goroutine. Receives on a closed channel return the zero value immediately, v, ok := <-ch tells you whether the channel was closed, and closing a channel wakes every waiting receiver at once.