Closing a channel indicates that no more values will be sent on it. This is useful to communicate completion to the channel’s receivers.
Basic Channel Closing
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
// Worker goroutine
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
done <- true
return
}
}
}()
// Send jobs
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
// Close the channel
close(jobs)
fmt.Println("sent all jobs")
// Wait for completion
<-done
}
The two-value receive form j, more := <-jobs returns false for more when the channel is closed and all values have been received.
The Two-Value Receive
- ok = true: Successfully received a value that was sent
- ok = false: Channel is closed and empty
Example
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
v1, ok1 := <-ch // v1=1, ok1=true
v2, ok2 := <-ch // v2=2, ok2=true
v3, ok3 := <-ch // v3=0, ok3=false (closed and empty)
Reading from Closed Channels
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
// Read remaining values
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
// Read from closed, empty channel
fmt.Println(<-ch) // 0 (zero value)
fmt.Println(<-ch) // 0 (zero value)
// Never blocks, always returns zero value
Receiving from a closed channel:
- Returns buffered values first
- Then returns the zero value of the channel’s type
- NEVER blocks
Checking if Channel is Closed
// Two-value receive
value, ok := <-ch
if ok {
fmt.Println("Received:", value)
} else {
fmt.Println("Channel closed")
}
// Common pattern in loops
for {
value, ok := <-ch
if !ok {
break // Channel closed
}
process(value)
}
Closing Rules
✅ Safe to Close
ch := make(chan int)
close(ch) // OK
❌ PANIC: Close Closed Channel
ch := make(chan int)
close(ch)
close(ch) // PANIC: close of closed channel
❌ PANIC: Send on Closed Channel
ch := make(chan int)
close(ch)
ch <- 1 // PANIC: send on closed channel
✅ OK: Receive from Closed Channel
ch := make(chan int)
close(ch)
value := <-ch // OK: returns zero value
Only the sender should close a channel, never the receiver. Closing a channel is a signal from the sender that no more values will be sent.
Common Patterns
1. Signal Completion
func worker(done chan bool) {
fmt.Println("working...")
time.Sleep(time.Second)
close(done) // Signal by closing
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Wait for close
}
2. Broadcast to Multiple Receivers
func broadcast() {
shutdown := make(chan struct{})
// Start multiple workers
for i := 0; i < 5; i++ {
go func(id int) {
<-shutdown // All unblock when channel closes
fmt.Printf("Worker %d shutting down\n", id)
}(i)
}
time.Sleep(2 * time.Second)
close(shutdown) // Wakes ALL goroutines
time.Sleep(time.Second)
}
Closing a channel wakes ALL receivers simultaneously. This is perfect for broadcast shutdown signals.
3. Producer Closes After Sending All
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // Signal: no more data
}
func consumer(ch <-chan int) {
for value := range ch { // Stops when channel closed
fmt.Println(value)
}
}
4. Fan-In with Close
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out) // Close when all inputs closed
}()
return out
}
Safe Close Pattern
type SafeChannel struct {
ch chan int
closed atomic.Bool
mu sync.Mutex
}
func (sc *SafeChannel) Close() {
sc.mu.Lock()
defer sc.mu.Unlock()
if !sc.closed.Load() {
close(sc.ch)
sc.closed.Store(true)
}
}
func (sc *SafeChannel) Send(value int) bool {
if sc.closed.Load() {
return false
}
sc.ch <- value
return true
}
Detecting Closed Channel Without Reading
// You can't directly detect if a channel is closed without receiving
// But you can use select with default:
func isClosed(ch <-chan int) bool {
select {
case _, ok := <-ch:
return !ok
default:
return false
}
}
This pattern is racy. The channel state may change immediately after the check. It’s generally better to just receive and check the ok value.
Graceful Shutdown Pattern
type Worker struct {
jobs <-chan Job
quit chan struct{}
done chan struct{}
}
func (w *Worker) Run() {
defer close(w.done)
for {
select {
case job, ok := <-w.jobs:
if !ok {
return // Jobs channel closed
}
w.process(job)
case <-w.quit:
return // Quit signal
}
}
}
func (w *Worker) Shutdown() {
close(w.quit)
<-w.done // Wait for worker to finish
}
Pipeline with Close Propagation
func stage1(out chan<- int) {
defer close(out) // Close output when done
for i := 0; i < 5; i++ {
out <- i
}
}
func stage2(in <-chan int, out chan<- int) {
defer close(out) // Close output when done
for num := range in { // Reads until in is closed
out <- num * 2
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go stage1(ch1)
go stage2(ch1, ch2)
for result := range ch2 { // Reads until ch2 is closed
fmt.Println(result)
}
}
Close Semantics Summary
| Operation | Behavior |
|---|
| Close closed channel | PANIC |
| Send to closed channel | PANIC |
| Receive from closed channel | Returns zero value |
| Close nil channel | PANIC |
value, ok := <-closedCh | Returns (zero, false) |
range over closed channel | Exits loop when drained |
Best Practices
- Sender closes - Only the sender should close a channel
- Close to signal completion - Use close to indicate “no more values”
- Use range when possible -
for v := range ch automatically handles close
- Check ok when needed - Use two-value receive in manual loops
- Close once - Never close a channel more than once
- Defer close - Use
defer close(ch) to ensure channels are closed
- Broadcast shutdown - Closing wakes all receivers
- Don’t close receiver-side - Receivers shouldn’t close channels
When to Close Channels
✅ Close when:
- Signaling completion to receivers
- No more values will be sent
- Broadcasting shutdown to multiple goroutines
- Ending a pipeline stage
❌ Don’t close when:
- Multiple senders (who closes?)
- Receiver wants to signal sender
- Not sure if already closed
- Channel might receive more values later
Multiple Senders Problem
// BAD: Multiple senders, who closes?
func badPattern() {
ch := make(chan int)
go func() {
ch <- 1
close(ch) // But what if other sender not done?
}()
go func() {
ch <- 2
close(ch) // PANIC: close of closed channel
}()
}
// GOOD: Coordinator closes
func goodPattern() {
ch := make(chan int, 10)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
ch <- 1
}()
go func() {
defer wg.Done()
ch <- 2
}()
go func() {
wg.Wait()
close(ch) // Close after all senders done
}()
}