Skip to main content
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

value, ok := <-channel
  • 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

OperationBehavior
Close closed channelPANIC
Send to closed channelPANIC
Receive from closed channelReturns zero value
Close nil channelPANIC
value, ok := <-closedChReturns (zero, false)
range over closed channelExits loop when drained

Best Practices

  1. Sender closes - Only the sender should close a channel
  2. Close to signal completion - Use close to indicate “no more values”
  3. Use range when possible - for v := range ch automatically handles close
  4. Check ok when needed - Use two-value receive in manual loops
  5. Close once - Never close a channel more than once
  6. Defer close - Use defer close(ch) to ensure channels are closed
  7. Broadcast shutdown - Closing wakes all receivers
  8. 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
	}()
}

Build docs developers (and LLMs) love