Understanding the Producer-Consumer Pattern in Go
In this piece
TL;DRProducers generate data, consumers process it, a channel in between absorbs the rate mismatch. Use it when production and consumption speeds diverge (network vs CPU, ingest vs analysis) or when you need to scale each side independently without coupling them.
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func consumer(ch <-chan int) {
for n := range ch {
fmt.Println(n)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
Producer-Consumer Pattern in Go
Introduction
The Producer-Consumer pattern is a fundamental concurrency pattern in Go that elegantly separates the production of data from its consumption. By using channels as intermediaries, this pattern creates a robust foundation for concurrent data processing.
When to Use
- When you need to separate data generation from its processing
- In scenarios where production and consumption rates differ
- When scaling producers and consumers independently is desired
- For managing workload distribution in concurrent systems
Why Use It
This pattern offers several compelling advantages:
- Modularity: Clear separation between data production and consumption logic
- Flexible Scaling: Easy to add more producers or consumers as needed
- Buffer Management: Handles different processing rates naturally
- Resource Control: Better management of system resources through controlled data flow
How it Works
The pattern consists of three main components:
- Producers: Goroutines that generate data
- Queue: A channel that acts as a buffer between producers and consumers
- Consumers: Goroutines that process the data
Simple Example
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("Produced: %d\n", i)
}
close(ch)
}
func consumer(ch <-chan int) {
for num := range ch {
fmt.Printf("Consumed: %d\n", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}Real-World Example: Web Scraper
type Page struct {
URL string
Content string
}
func scraper(urls []string, pages chan<- Page) {
for _, url := range urls {
// Simulate web scraping
time.Sleep(100 * time.Millisecond)
pages <- Page{
URL: url,
Content: fmt.Sprintf("Content from %s", url),
}
}
close(pages)
}
func processor(pages <-chan Page, results chan<- string) {
for page := range pages {
// Simulate content processing
time.Sleep(200 * time.Millisecond)
results <- fmt.Sprintf("Processed %s: %s", page.URL, page.Content)
}
close(results)
}
func main() {
urls := []string{"https://example1.com", "https://example2.com"}
pages := make(chan Page)
results := make(chan string)
go scraper(urls, pages)
go processor(pages, results)
for result := range results {
fmt.Println(result)
}
}Best Practices and Pitfalls
Best Practices:
- Always use directional channel types (
chan<-for send-only,<-chanfor receive-only) - Consider buffered channels when producers and consumers work at different speeds
- Implement proper error handling and propagation
- Use context for cancellation when needed
Common Pitfalls:
- Forgetting to close channels
- Not handling backpressure when producers are faster than consumers
- Creating deadlocks by improper channel management
- Memory leaks from unclosed goroutines
Summary
The Producer-Consumer pattern is a cornerstone of concurrent programming in Go. Its simplicity and effectiveness make it an excellent starting point for learning concurrency patterns. By separating concerns and managing data flow through channels, it provides a clean and efficient way to handle concurrent data processing tasks.
Disclaimer
This article is a simple introduction to the Producer-Consumer pattern in Go. For more advanced use cases and optimizations, consider exploring additional resources and best practices in concurrent programming. If you want to have a look at the code examples, you can find them on my GitHub repository.
Filed under
Next in Go Patterns
Generator Pattern in Go: Lazy Sequences with Channels
Part 3 continues the series.