Buffered Channels
By default, channels in Go are unbuffered, meaning they will only accept sends (chan <-
) if there is a corresponding receiver (<-chan
) ready to receive the value. Buffered channels, on the other hand, can hold a limited number of values without a corresponding receiver.
Creating a Buffered Channel#
To create a buffered channel, you specify the buffer size as the second argument to make
:
string">"comment">// Create a buffered channel with capacity of 2
messages := make(chan string, 2)
How Buffered Channels Work#
Buffered channels work like queues:
- When you send a value, it's added to the end of the queue
- When you receive a value, it's taken from the front of the queue
- If the queue is full, sends will block
- If the queue is empty, receives will block
Example of a Buffered Channel#
Here's a simple example demonstrating buffered channels:
package main
import string">"fmt"
func main() {
string">"comment">// Create a buffered channel with a capacity of 2
messages := make(chan string, 2)
string">"comment">// Send two values into the channel without a receiver
messages <- string">"buffered"
messages <- string">"channel"
string">"comment">// Receive the two values
fmt.Println(<-messages) string">"comment">// Output: buffered
fmt.Println(<-messages) string">"comment">// Output: channel
}
Notice how in this example:
- We can send two values into the channel without any goroutine ready to receive
- The sends don't block because the buffer isn't full yet
- If we tried to send a third value without first receiving any, the send would block
When to Use Buffered Channels#
Buffered channels are useful in several scenarios:
- Reducing goroutine blocking: When the sender doesn't need to wait for the receiver to be ready
- Bursty workloads: When the rate of sends and receives varies over time
- Batching operations: When you want to process items in batches rather than one at a time
- Producer/consumer patterns: When producers might temporarily produce faster than consumers can consume
Choosing a Buffer Size#
Selecting an appropriate buffer size depends on your specific use case:
- Too small: May cause unnecessary blocking and reduce concurrency
- Too large: May hide synchronization bugs and consume too much memory
- Just right: Provides enough capacity to smooth out temporary rate differences
As a general rule, use the smallest buffer size that prevents unnecessary blocking.
Example: Worker Pool with Buffered Channels#
Here's a more practical example using buffered channels to implement a worker pool:
package main
import (
string">"fmt"
string">"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf(string">"Worker %d processing job %d\n", id, job)
time.Sleep(time.Second) string">"comment">// Simulate work
results <- job * 2
}
}
func main() {
string">"comment">// Create buffered channels
jobs := make(chan int, 100)
results := make(chan int, 100)
string">"comment">// Start workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
string">"comment">// Send jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
string">"comment">// Collect results
for a := 1; a <= 9; a++ {
<-results
}
}
In this example:
- The buffered
jobs
channel allows us to queue up jobs without waiting for workers - The buffered
results
channel allows workers to report results without waiting for the main goroutine
Channel Blocking Behavior with Buffers#
Understanding when buffered channels block is crucial:
-
Send operation (
ch <- value
):- Blocks when the buffer is full
- Doesn't block when the buffer has space
-
Receive operation (
<-ch
):- Blocks when the buffer is empty
- Doesn't block when the buffer has at least one value
Deadlocks with Buffered Channels#
Even with buffered channels, deadlocks can still occur. Common causes include:
- Buffer too small for the number of sends
- Forgetting to close channels
- Circular dependencies between goroutines
Performance Considerations#
Some performance characteristics to keep in mind:
- Unbuffered channels are slightly faster than buffered channels for synchronization
- Larger buffer sizes don't necessarily mean better performance
- The optimal buffer size depends on your specific workload patterns
Summary#
- Buffered channels have a capacity that allows them to hold multiple values
- They're created with
make(chan Type, capacity)
- Sends only block when the buffer is full
- Receives only block when the buffer is empty
- They're useful for decoupling senders and receivers
- Choose buffer sizes carefully based on your specific use case
In the next lab, we'll explore how to create and work with goroutines, Go's lightweight threads for concurrent execution.