Go

Go Channels👨‍💻

Channels are Go's primary mechanism for communication between goroutines. The core philosophy — do not communicate by sharing memory; share memory by communicating — means that instead of protecting shared variables with mutexes, you pass ownership of data from one goroutine to another through a channel. When a value is sent on a channel, the sender relinquishes access, and the receiver takes ownership. Data races become structurally impossible when this discipline is followed.

A channel is a typed conduit created with make(chan T). Sends block until a receiver is ready (unbuffered) or until buffer space is available (buffered). Receives block until a value is available. This blocking behavior is not a limitation — it is the synchronization mechanism. Combined with the select statement for multiplexing, range for iteration, and close for signaling completion, channels enable expressive concurrency patterns: pipelines that chain processing stages, fan-out that distributes work across workers, fan-in that merges results, and cancellation that propagates through an entire call graph.

Channels are reference types backed by a runtime data structure that includes a mutex, a circular buffer (for buffered channels), and wait queues for blocked senders and receivers. They are safe for concurrent use without additional synchronization. Directional channel types (chan<- for send-only, <-chan for receive-only) encode intent in function signatures, making APIs self-documenting and preventing misuse at compile time.

Key Takeaways

  • 1Unbuffered channels (`make(chan T)`) synchronize sender and receiver — the send blocks until another goroutine receives, guaranteeing a handoff at a known point in time
  • 2Buffered channels (`make(chan T, n)`) decouple sender and receiver up to the buffer capacity — sends only block when the buffer is full, receives only block when it is empty
  • 3Directional channel types (`chan<- T` for send-only, `<-chan T` for receive-only) enforce correct usage at compile time and make function signatures self-documenting
  • 4The `select` statement multiplexes across multiple channel operations — it blocks until one case is ready, and a `default` case makes it non-blocking
  • 5`range` over a channel receives values until the channel is closed, providing a clean iteration pattern for consumer goroutines
  • 6Closing a channel is a broadcast signal: all pending and future receives return immediately with the zero value, making it the standard way to signal completion to multiple goroutines

Master go channels

Take the Go Programming course with hands-on lessons and challenges.

Examples

Unbuffered vs buffered channels

go

An unbuffered channel forces a rendezvous: the sender blocks until a receiver is ready, creating a synchronization point. A buffered channel with capacity 2 lets the sender deposit up to 2 values without blocking. Choose unbuffered when you need synchronization guarantees, buffered when you need to decouple producer and consumer speeds.

Select statement for multiplexing channels

go

The select statement waits on multiple channel operations simultaneously and executes whichever case becomes ready first. Here it receives from fast and slow concurrently, with a context timeout as a deadline. If the slow result does not arrive before the timeout, the ctx.Done() case fires and the function returns without leaking the loop.

Pipeline pattern — chaining processing stages

go

Each stage is a goroutine that reads from an inbound channel and writes to an outbound channel. Stages compose by chaining return values. Closing the outbound channel signals downstream stages that no more data is coming, so range loops terminate naturally. This is the foundational pattern described in the Go blog's pipeline article.

Fan-out / fan-in for parallel processing

go

Fan-out distributes work by having multiple goroutines read from the same channel — the runtime ensures each value goes to exactly one reader. Fan-in collects results by merging multiple output channels into one using a WaitGroup to know when all workers are done. The merged channel closes only after every input channel is drained.

Directional channels in function signatures

go

Directional channel types enforce at compile time that a function can only send or only receive. The bidirectional channel created in main is implicitly narrowed when passed to produce (send-only) and consume (receive-only). This prevents bugs where a producer accidentally reads from its output channel or a consumer accidentally writes to its input.

Semaphore pattern with a buffered channel

go

A buffered channel of empty structs acts as a counting semaphore. Sending to it acquires a slot (blocking when full), receiving releases a slot. This limits concurrency to maxConcurrent goroutines at any time, regardless of how many were launched. The empty struct{} costs zero bytes of allocation.

Common Mistakes

Mistake:

Sending on a closed channel — this causes an unrecoverable panic at runtime, and it can be hard to trace when multiple goroutines share a channel

Fix:

Only the sender should close a channel, and only when all sends are complete. If multiple goroutines send on the same channel, use a sync.WaitGroup to close it after all senders finish. Never close from the receiver side.

Mistake:

Forgetting to close a channel that a consumer ranges over — the range loop blocks forever waiting for more values, causing a goroutine leak and potential deadlock

Fix:

Every channel that a goroutine ranges over must eventually be closed by the sender. Structure your code so the producer goroutine defers close(ch) immediately after creating the channel.

Mistake:

Using a buffered channel to avoid synchronization issues instead of addressing the underlying design problem — large buffers mask race conditions and timing bugs that surface under load

Fix:

Choose buffer sizes based on the semantics you need, not as a band-aid. Buffer size 0 (unbuffered) for synchronization guarantees, small buffers (1-10) to absorb bursts, and larger buffers only when you have measured throughput requirements.

Mistake:

Reading from a nil channel — this blocks forever with no panic or error, silently stalling the goroutine

Fix:

A nil channel is useful in select statements to disable a case dynamically (set the channel variable to nil after it is closed to stop selecting on it). Outside of select, ensure channels are always initialized with make().

Best Practices

  • Design channel ownership clearly: the goroutine that creates and sends on a channel should be the one that closes it. Document this in your function signatures using directional types (chan<- for producer, <-chan for consumer).
  • Prefer unbuffered channels as the default. They provide the strongest synchronization guarantees and make data flow explicit. Only add a buffer when profiling shows that the synchronous handoff is a bottleneck.
  • Use select with a context.Done() or timeout case on every channel operation that could block indefinitely. This prevents goroutine leaks when the other side of the channel is not cooperating.
  • Use the comma-ok idiom (`val, ok := <-ch`) when you need to distinguish between a real zero value and a closed channel. If ok is false, the channel is closed and drained — stop reading.
  • Keep pipelines shallow. A pipeline with more than 3-4 stages becomes hard to reason about and debug. If the processing chain is complex, consider collapsing adjacent stages or using an explicit task queue instead.

Summary

Channels are Go's typed communication primitives for goroutines. Unbuffered channels synchronize sender and receiver; buffered channels decouple them up to a capacity. The select statement multiplexes across channels, directional types enforce send/receive roles at compile time, and closing a channel broadcasts completion to all readers. Combined into patterns — pipelines, fan-out/fan-in, semaphores — channels let you build concurrent systems where data flows through well-defined paths without shared mutable state.

Practice Go with hands-on challenges

Learn go channels hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.