Menu

Golang Channels: Buffered, Unbuffered, Close and Range

How Go channels pass values between goroutines: unbuffered and buffered channels, closing and ranging, direction types, the deadlock error, and a pipeline built from them.

This page includes runnable editors - edit, run, and see output instantly.

Sending and receiving

A channel is a typed pipe between goroutines. ch <- v sends, <-ch receives. Create one with make:

The zero value of a channel type is nil, so var ch chan string without make gives you a channel that blocks forever. Always create channels with make.

Unbuffered channels synchronize

make(chan T) creates an unbuffered channel. A send blocks until a receiver takes the value, and a receive blocks until a sender provides one. The two goroutines meet at that point, which makes an unbuffered channel a synchronization tool as much as a data pipe. When <-done returns below, you know the worker has finished everything before its send:

Reading result in main is safe here without a mutex. Go's memory model guarantees that everything the worker did before the send is visible to main after the matching receive. chan struct{} is the idiomatic type for a pure signal, because struct{} takes no memory.

Buffered channels

make(chan T, n) gives the channel room for n values. Sends succeed without a receiver until the buffer is full; receives succeed until it is empty. Values come out in the order they went in.

A buffer decouples sender and receiver so that short bursts do not stall the sender. It does not fix a producer that is permanently faster than its consumer; it only delays the moment the sender blocks. Pick a buffer size for a reason (the number of senders, a known batch size), not to make a deadlock go away.

len(ch) is a snapshot. By the time you act on it another goroutine may have changed it, so do not use it to decide whether a send will block. Use select with a default case for that.

Close and range

close(ch) tells receivers that no more values will be sent. After a close:

  • values already in the buffer are still delivered,
  • then every receive returns the zero value immediately,
  • v, ok := <-ch reports ok == false,
  • for v := range ch ends.

The first receive after close still gets "last" with ok == true. The second gets the zero value "" and false.

Rules that cause panics:

  • sending on a closed channel panics with send on closed channel,
  • closing an already closed channel panics,
  • closing a nil channel panics.

So only the sending side closes, and only once. With several senders, no single sender knows when the others are done; have a separate goroutine wait for all senders (with a sync.WaitGroup) and close the channel after Wait returns. Closing is only needed when a receiver waits for the end. An unclosed channel that nobody references is garbage collected like any other value.

Direction types

A function can declare that it only sends or only receives on a channel. The compiler then rejects the other operation.

TypeMeaningAllowed
chan Tbidirectionalsend, receive, close
chan<- Tsend-onlysend, close
<-chan Treceive-onlyreceive

A chan T converts implicitly to either restricted type when you pass it to a function. That is why produce above can return a <-chan int: callers can range over it but cannot send into it or close it. Use direction types on every function parameter where they apply. They document ownership and turn misuse into a compile error.

Deadlock

If every goroutine is blocked and nothing can wake any of them, the runtime aborts the program:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

The goroutine dump tells you which operation is stuck (chan send here, or chan receive, sync.WaitGroup.Wait, select). The usual causes:

  • a send on an unbuffered channel with no receiver running,
  • range over a channel that is never closed,
  • a WaitGroup counter that never reaches zero,
  • two goroutines each waiting for the other.

The runtime only detects the case where all goroutines are stuck. In a server with other goroutines alive (an HTTP listener, a ticker), the same bug does not crash anything; the blocked goroutine just leaks.

Nil channels

Sends and receives on a nil channel block forever. That sounds useless but is a standard trick inside select: setting a channel variable to nil disables its case. This merges two channels and stops listening to each one once it is closed:

Without the nil assignments, a closed channel is always ready, and the loop would spin on zero values.

A pipeline

Channels compose into pipelines: each stage is a goroutine that receives from one channel and sends to the next, and closes its output when its input is done.

Each stage runs concurrently, and the output order is deterministic (1, 16, 81) because every stage is a single goroutine that preserves order. The closes cascade: generate closes, which ends square's range, which closes its output, and so on down to main.

The weak point of this pipeline: if main stopped reading early, the stages would block on their sends forever. Real pipelines take a context.Context or a done channel and select on it next to every send.

Channel or mutex

Channels are for passing ownership of data and for signaling events. A sync.Mutex is simpler for protecting shared state that many goroutines read and update in place, like a cache or a counter. A struct with a mutex inside is often clearer than a goroutine that owns the state and serves requests over channels. Use whichever makes the code shorter and the ownership obvious.

Quick reference

Operationnil channelopen channelclosed channel
ch <- vblocks foreverblocks until received or buffer has roompanics
<-chblocks foreverblocks until a value is availablebuffered values, then zero value
v, ok := <-chblocks foreverok is trueok is false once drained
close(ch)panicsclosespanics
len(ch), cap(ch)0, 0values buffered, buffer sizevalues left, buffer size

Frequently Asked Questions

What is the difference between a buffered and an unbuffered channel in Go?

An unbuffered channel (make(chan int)) has no storage: a send blocks until another goroutine receives, so every send is also a handoff and a synchronization point. A buffered channel (make(chan int, 3)) holds up to 3 values; sends block only when the buffer is full and receives block only when it is empty.

What happens when you read from a closed channel in Go?

Receives on a closed channel never block. They first drain any values still in the buffer, then return the zero value of the element type forever. Use v, ok := <-ch to tell the difference: ok is false once the channel is closed and empty. A for v := range ch loop stops at that point.

Who should close a channel in Go?

The sender, and only when receivers need to know that no more values are coming (for example to end a range loop). Sending on a closed channel panics, and so does closing a channel twice, so a receiver closing the channel races with the senders. You do not have to close a channel to free it; the garbage collector reclaims unreachable channels either way.

What does "fatal error: all goroutines are asleep" mean?

Every goroutine in the program is blocked on a channel operation or lock that nothing can ever complete, so the runtime stops the program. The most common cause is sending on an unbuffered channel in main with no other goroutine receiving, or ranging over a channel that is never closed.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED