Channels: Buffered, Unbuffered & Closing Rules
Channels carry values and synchronization between goroutines.
Search across all documentation pages
Channels carry values and synchronization between goroutines.
Choosing buffered versus unbuffered capacity and following close ownership rules prevents deadlocks and panics in production pipelines.
An unbuffered channel synchronizes partners: the sender blocks until the receiver is ready.
A buffered channel stores up to cap values without a waiting receiver.
Closing a channel broadcasts "no more sends" to receivers; only senders should close.
range over a channel drains until close; the two-value receive v, ok := <-ch detects close explicitly.
Quick-reference recipe card - copy-paste ready.
package main
import "fmt"
func main() {
ch := make(chan int, 2) // buffered
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
v, ok := <-ch
fmt.Println(v, ok) // 0 false after drained close
}When to reach for this:
range loops.package main
import (
"fmt"
"sync"
)
func producer(out chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
out <- i
}
close(out) // sender closes
}
func consumer(in <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for v := range in {
fmt.Println("got", v)
}
}
func main() {
ch := make(chan int) // unbuffered - sync handoff
var wg sync.WaitGroup
wg.Add(2)
go producer(ch, &wg)
go consumer(ch, &wg)
wg.Wait()
}What this demonstrates:
chan<- and <-chan document send-only/receive-only usage.range in consumer terminates.WaitGroup waits for both sides to finish.close(ch) sets closed flag; future receives get zero value with ok == false.len(ch) and cap(ch) report queued elements and capacity.| Type | Send blocks when | Receive blocks when | Sync strength |
|---|---|---|---|
| Unbuffered | No receiver ready | No sender ready | Strong (rendezvous) |
| Buffered | Buffer full | Buffer empty | Weaker until full |
| Rule | Rationale |
|---|---|
| Sender closes | Receivers should not signal producer completion |
| Close once | Double close panics |
Receivers detect via ok or range | Clean shutdown of consumer loops |
| Do not close work channels early | Consumers may still expect values |
// Fan-in: one goroutine closes after all senders done
func fanIn(inputs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(inputs))
for _, in := range inputs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(in)
}
go func() {
wg.Wait()
close(out)
}()
return out
}sync.Once to guard close.context cancel, timeout select, or buffer.close or cancel upstream.WaitGroup then close).
| Alternative | Use When | Don't Use When |
|---|---|---|
| Unbuffered channel | Need strict handoff | High throughput with mismatched rates |
| Buffered channel | Burst absorption | You need immediate backpressure signals |
sync.Mutex + slice queue | Shared in-memory queue | Cross-goroutine ownership transfer is clearer with channels |
context only | Cancellation signaling | Passing work items (use channels) |
Start with 0 (unbuffered) or a small multiple of worker count.
Benchmark and watch queue depth under load; oversized buffers hide overload.
Yes - sends are safe concurrently.
Coordinate close so only one goroutine closes after all senders finish.
Blocks forever.
Use a nil channel in select to disable a case dynamically.
Receivers blocked forever keep senders alive regardless of close.
Close is for semantics and terminating range, not primarily GC.
Technically possible in toy examples; in pipelines it breaks ownership.
Receivers should exit on close, not initiate it.
nil - operations on nil channels block forever (useful in select tricks).
chan<- T and <-chan T in function signatures document intent and prevent accidental receive/send at compile time.
Yes - each receive copies.
Send pointers if mutating shared structs (with synchronization).
Receivers drain buffered values after close, then get ok == false.
A send on a channel happens-before the corresponding receive completes.
That edge is part of Go's memory model.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026