Skip to main content
โšก Calmops

Buffered vs Unbuffered Channels

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

  1. Use unbuffered for synchronization - Clear intent
  2. Use buffered for decoupling - Different rates
  3. Close channels from sender - Signal completion
  4. Use directional channels - <-chan and chan<-
  5. Avoid sending on closed channels - Panic
  6. Use select for timeouts - Prevent hangs
  7. Document channel semantics - Explain usage
  8. 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

Tools and Resources

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