Channels are typed queues with blocking semantics coordinated by the runtime. An unbuffered channel forces a rendezvous between the sender and receiver; a buffered channel stores values in a fixed ring until its capacity is full. Closing a channel changes the send/receive rules and serves as the idiomatic broadcast signal.
A channel variable is a descriptor pointing to an hchan structure that holds a buffer, queues of goroutines waiting to send or receive, and lock-based synchronization mechanisms. Understanding the blocking points is crucial to prevent deadlocks, memory leaks, and misuse of the close function.
Quick-reference recipe card - ready to copy and paste.
ch := make(chan int) // unbuffered - synchronizes handoffbuf := make(chan int, 8) // buffered - accepts up to 8 sends without a receiverbuf <- 1v := <-bufclose(buf)v, ok := <-buf // ok will be false after the buffer is drained
When to use this:
Signaling between goroutines with happens-before guarantees.
Worker pools with a bounded backlog (buffered channels).
Pipeline stages that hand off ownership of pointers.
Broadcasting a shutdown signal after close (allowing receivers to exit their loops).
package mainimport ( "fmt" "sync")func main() { ch := make(chan int, 2) // Create a buffered channel with capacity 2 var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() ch <- 1 // Send value 1 ch <- 2 // Send value 2 close(ch) // Close the channel after sending all values }() // Range over the channel to receive values for v := range ch { fmt.Println(v) // Print received value } wg.Wait() // Wait for the goroutine to finish}
What this demonstrates:
A buffered channel can accept two sends without a receiver waiting.
close allows the range loop to drain any remaining values from the channel and then exit.
In most designs, only the sender should close the channel.
hchan structure: Contains a buffer ring, qcount (queue count), send/receive wait queues (implemented as sudog linked lists), a lock, and information about the element type.
Unbuffered send: Blocks until a receiver executes a receive operation. Values are copied directly from the sender goroutine to the receiver goroutine (handoff).
Buffered send: Copies the value into the buffer if there is space. It blocks only when the buffer is full, waiting for a receive operation to free up a slot.
Receive: Blocks if the channel is empty (unless it's closed). If the channel is closed and empty, it returns the zero value for the channel's type and ok == false.
close(ch): After closing, no more values can be sent on the channel. Receivers can still retrieve any remaining buffered values. Subsequent receives on the channel will return the zero value for the channel's type with ok == false.
Multiple receivers: If multiple goroutines are blocked on receiving from a channel, all of them will be woken up when a value is sent (or when the channel is closed). However, only one receiver will successfully acquire each value.
select with default: Non-blocking operations can be implemented using a select statement with a default case, which executes immediately if no other case is ready.
// Ownership: The sender closes the channel when it's done producing values.go func() { defer close(out) // Ensure the channel is closed when the goroutine finishes for item := range work { // Process items from the 'work' channel out <- process(item) // Send the processed item to the 'out' channel }}()// Do not close from the receiver side unless the protocol explicitly dictates it.// The 'range' loop automatically exits when the channel is closed and drained.
Closing by receiver: Closing a channel from a goroutine other than the designated sender can lead to race conditions with ongoing sends. Fix: Clearly document which goroutine is responsible for closing the channel. Use sync.Once if the closing pattern is complex but well-defined.
Send on closed channel: Attempting to send a value on a closed channel results in an immediate panic. Fix: Guard send operations with a select statement that checks for context cancellation (ctx.Done()) or ensure that all sends are completed before closing the channel.
Double close: Closing an already closed channel also causes a panic. Fix: Ensure that only a single goroutine is responsible for closing the channel. Other goroutines should signal their completion or intent through other means, such as a context.
Buffered size 0 vs. unbuffered: A channel created with make(chan T, 0) is functionally identical to an unbuffered channel. This can be confusing if the intent was to create a buffered channel but no capacity was specified. Fix: Omit the capacity argument (make(chan T)) for channels that require synchronization at the point of handoff. Specify an explicit buffer size (make(chan T, N)) when throughput is more important than immediate synchronization.
Leaked goroutine blocked on send: If a goroutine is blocked indefinitely on a send operation because there is no receiver, and there is no mechanism for cancellation, it can lead to a goroutine leak. Fix: Combine channel operations with context cancellation and select statements to provide a way to unblock and terminate goroutines.
Using channel length for synchronization: Relying on len(ch) for synchronization is unreliable because it provides only a snapshot of the number of elements currently in the buffer. It does not guarantee a specific queue depth contract. Fix: If the protocol requires tracking counts, use explicit counter variables managed atomically or with mutexes.
What is the difference between buffered and unbuffered channels?
An unbuffered channel synchronizes the sender and receiver at the exact moment of the handoff. A buffered channel allows send operations to proceed without immediately blocking, up to its defined capacity, even if there isn't a receiver waiting.
Who should close a channel?
Typically, the sender or producer goroutine should close the channel when it determines that no more values will be sent. Receivers can detect the channel's completion either by checking the ok boolean returned from a receive operation or by using a range loop, which automatically terminates when the channel is closed and drained.
What happens when receiving from a closed channel?
After a channel is closed and all buffered values have been received, any subsequent receive operations will return immediately without blocking. They will yield the zero value for the channel's element type and ok will be false.
Why does sending on a closed channel cause a panic?
This behavior is intentional and indicates a protocol violation or bug in the program's logic. Sending on a closed channel implies that producers and consumers disagree about the channel's lifecycle, which is a critical error.
What does a nil channel do?
A nil channel is a channel that has not been initialized. Any attempt to send to or receive from a nil channel will block indefinitely. This behavior is often useful in select statements to dynamically disable a particular case.
Is channel receive fair?
While the Go runtime typically wakes waiting goroutines in FIFO (First-In, First-Out) order for channel wait queues in most common scenarios, you should not rely on this fairness for program correctness. Design your concurrency patterns assuming potential unfairness.
Can I peek at a channel's length?
Yes, you can use the built-in len(ch) function to get the number of elements currently queued in the channel's buffer, and cap(ch) to get the buffer's total capacity. However, both len and cap return snapshots of the channel's state and can be subject to race conditions if used for synchronization in concurrent programs.
Are channels copyable?
Channel variables themselves are descriptors. When you copy a channel variable, you are duplicating the handle to the same underlying hchan structure. Both the original and the copied variable refer to the same channel.
Do channels provide happens-before guarantees?
Yes. A send operation on a channel happens before the corresponding receive operation completes. Similarly, a close operation happens before any receive operation that returns the zero value with ok == false.
How large should a channel buffer be?
The buffer size should be sufficient to absorb temporary bursts from the producer without causing it to block, thus improving throughput. However, it should also be small enough to apply backpressure to the producer if the consumer cannot keep up, preventing unbounded memory growth.
Can I range over a nil channel?
No, ranging over a nil channel will block forever, similar to attempting a receive operation on a nil channel.
Are channels comparable?
Channels can only be compared to nil using the == operator. Comparing two non-nil channels for equality is not permitted.
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).
Revisado por Chris St. John·Última actualización: 16 jul 2026