Buffered vs Unbuffered Channels
Channels are Go’s primary mechanism for communication between goroutines. Understanding the difference between buffered and unbuffered channels is crucial for writing correct concurrent programs. This guide covers both types and their use cases.
Unbuffered Channels
Basic Unbuffered Channel
package main
import (
"fmt"
"time"
)
func main() {
// Create unbuffered channel
ch := make(chan int)
go func() {
fmt.Println("Sending value...")
ch <- 42 // Blocks until someone receives
fmt.Println("Value sent")
}()
time.Sleep(1 * time.Second)
fmt.Println("Receiving value...")
value := <-ch // Blocks until someone sends
fmt.Println("Received:", value)
}
Synchronization with Unbuffered Channels
package main
import (
"fmt"
"sync"
)
func worker(id int, ch chan string) {
fmt.Printf("Worker %d starting\n", id)
ch <- fmt.Sprintf("Worker %d done", id)
}
func main() {
ch := make(chan string)
// Start workers
for i := 1; i <= 3; i++ {
go worker(i, ch)
}
// Wait for all workers
for i := 1; i <= 3; i++ {
fmt.Println(<-ch)
}
}
Buffered Channels
Basic Buffered Channel
package main
import "fmt"
func main() {
// Create buffered channel with capacity 2
ch := make(chan int, 2)
// Can send without blocking (up to capacity)
ch <- 1
ch <- 2
fmt.Println("Sent 2 values")
// Receive values
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
Buffered Channel Behavior
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 3)
// Send 3 values without blocking
ch <- 1
ch <- 2
ch <- 3
fmt.Println("Sent 3 values")
// This would block (buffer full)
// ch <- 4 // Deadlock!
// Receive to make space
fmt.Println(<-ch) // 1
// Now we can send
ch <- 4
fmt.Println("Sent 4th value")
}
Comparing Buffered and Unbuffered
Unbuffered Channels
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) // Unbuffered
go func() {
fmt.Println("Goroutine: sending")
ch <- 42
fmt.Println("Goroutine: sent")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Main: receiving")
value := <-ch
fmt.Println("Main: received", value)
}
// Output:
// Goroutine: sending
// Main: receiving
// Goroutine: sent
// Main: received 42
Buffered Channels
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1) // Buffered with capacity 1
go func() {
fmt.Println("Goroutine: sending")
ch <- 42
fmt.Println("Goroutine: sent")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Main: receiving")
value := <-ch
fmt.Println("Main: received", value)
}
// Output:
// Goroutine: sending
// Goroutine: sent
// Main: receiving
// Main: received 42
Practical Patterns
Worker Pool with Buffered Channel
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(100 * time.Millisecond)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// Start workers
for i := 1; i <= 3; i++ {
go worker(i, jobs, results)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for i := 1; i <= 5; i++ {
fmt.Println("Result:", <-results)
}
}
Rate Limiting with Buffered Channel
package main
import (
"fmt"
"time"
)
func main() {
// Limit to 3 concurrent operations
limiter := make(chan struct{}, 3)
for i := 1; i <= 10; i++ {
go func(id int) {
limiter <- struct{}{} // Acquire
defer func() { <-limiter }() // Release
fmt.Printf("Processing %d\n", id)
time.Sleep(100 * time.Millisecond)
}(i)
}
time.Sleep(2 * time.Second)
}
Timeout Pattern
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
// Wait with timeout
select {
case result := <-ch:
fmt.Println("Got result:", result)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}
Deadlock Prevention
Common Deadlock: Unbuffered Channel
package main
func main() {
ch := make(chan int)
// โ Deadlock: sending without receiver
ch <- 42
// This line never executes
value := <-ch
}
Fix: Use Goroutine
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)
}
Common Deadlock: Buffered Channel
package main
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2 // โ Deadlock: buffer full
}
Fix: Receive Before Sending
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch) // Make space
ch <- 2
fmt.Println(<-ch)
}
When to Use Each
Use Unbuffered Channels When:
// Synchronization between goroutines
done := make(chan bool)
go func() {
// Do work
done <- true
}()
<-done // Wait for completion
// Request-response pattern
request := make(chan string)
response := make(chan string)
go func() {
req := <-request
response <- process(req)
}()
Use Buffered Channels When:
// Producer-consumer with different rates
jobs := make(chan int, 10)
go producer(jobs)
go consumer(jobs)
// Rate limiting
limiter := make(chan struct{}, 5)
// Collecting results
results := make(chan int, 100)
Best Practices
โ Good Practices
- Use unbuffered for synchronization - Clear intent
- Use buffered for decoupling - Different rates
- Close channels from sender - Signal completion
- Use directional channels -
<-chanandchan<- - Avoid sending on closed channels - Panic
- Use select for timeouts - Prevent hangs
- Document channel semantics - Explain usage
- Test for deadlocks - Use race detector
โ Anti-Patterns
// โ Bad: Sending on closed channel
ch := make(chan int)
close(ch)
ch <- 1 // Panic!
// โ
Good: Close from sender
go func() {
ch <- 1
close(ch)
}()
// โ Bad: Bidirectional channels
func process(ch chan int) {
// Unclear if sending or receiving
}
// โ
Good: Directional channels
func send(ch chan<- int) {
ch <- 1
}
func receive(ch <-chan int) {
<-ch
}
// โ Bad: Unbuffered for high throughput
ch := make(chan int)
for i := 0; i < 1000; i++ {
ch <- i
}
// โ
Good: Buffered for throughput
ch := make(chan int, 100)
Resources and References
Official Documentation
- Channels Documentation - Language specification
- Effective Go - Channels - Best practices
- Concurrency Patterns - Official blog series
Recommended Reading
- Go Concurrency Patterns - Rob Pike talk (video)
- Advanced Go Concurrency Patterns - Rob Pike talk (video)
- Channel Axioms - Code review guide
Tools and Resources
- Race Detector - Detect race conditions
- Delve Debugger - Debug goroutines
- Go Playground - Online Go editor
Summary
Buffered and unbuffered channels serve different purposes:
- Unbuffered for synchronization
- Buffered for decoupling
- Close from sender
- Use directional channels
- Prevent deadlocks with select
- Test with race detector
Master channels for correct concurrent Go programs.
Comments