Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 23

    Buffered Channels

    Buffered channels have a capacity — sends succeed without blocking until the buffer fills.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Buffered channels have a capacity — sends succeed without blocking until the buffer fills. They implement queues, semaphores, and backpressure in high-throughput systems. Choosing the right buffer size affects latency, memory, and deadlock risk.

    Understanding when buffer size 0 (sync), 1 (handoff), or N (pipeline) is appropriate separates production-ready concurrent code from tutorial examples. This lesson covers semaphore patterns, rate limiting, and avoiding buffer-related deadlocks.

    Buffered channels appear in worker pools, logging pipelines, and Kubernetes controller work queues — patterns you will implement in the final project.

    The story

    A Cloudflare log ingestion worker uses a buffered channel of capacity 1000 to decouple HTTP handlers from slow S3 uploads. Handlers enqueue log batches without blocking until the buffer fills — then backpressure signals the load balancer to shed traffic before the process OOMs.

    Choosing buffer size is an engineering trade-off: too small adds latency under burst; too large hides backpressure until memory exhaustion — production tuning uses metrics on channel length via custom instrumentation.

    Understanding the topic

    Key concepts

    • make(chan T, n) creates buffer of n elements.
    • len(ch) current items; cap(ch) buffer capacity.
    • Send non-blocking when len < cap; blocks when full.
    • Receive non-blocking when len > 0; blocks when empty.
    • Semaphore: chan struct{} with cap N limits concurrency.
    • Too large buffer hides backpressure until OOM.

    Step-by-step explanation

    1. Producer sends until buffer full, then blocks.
    2. Consumer receives, freeing slot for blocked sender.
    3. Zero buffer: synchronous handoff.
    4. Size-1 buffer: async handoff with one in-flight item.
    5. Close drains remaining buffer then signals done.
    6. Select with default enables non-blocking try-send/receive.

    Practical code example

    Semaphore limiting concurrent API calls with buffered channel:

    go
    package main
    import (
    "fmt"
    "sync"
    "time"
    )
    func fetch(id int, sem chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    sem <- struct{}{} // acquire
    defer func() { <-sem }() // release
    fmt.Printf("fetch %d started\n", id)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("fetch %d done\n", id)
    }
    func main() {
    const maxConcurrent = 2
    sem := make(chan struct{}, maxConcurrent)
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
    wg.Add(1)
    go fetch(i, sem, &wg)
    }
    wg.Wait()
    }

    Line-by-line code explanation

    • ch := make(chan LogBatch, 1000) creates a buffered channel with capacity 1000.
    • ch <- batch sends without blocking while buffer has space.
    • len(ch) returns current queued items — useful for metrics/alerts.
    • cap(ch) returns the buffer capacity (1000 in this example).
    • blocking send when full — producers wait when buffer is at capacity, applying backpressure.
    • worker pool pattern — N workers drain a buffered job channel concurrently.
    • time.After with select adds timeout when buffer stays full too long.
    • prefer unbuffered for synchronization — add buffering only when profiling shows benefit.

    Key takeaway: Buffered channel as counting semaphore. maxConcurrent=2 limits parallel fetches. Adjust buffer to match resource limits (DB connections, API rate).

    Real-world use

    Where you'll use this in production

    • Limiting concurrent database queries to pool size.
    • Job queue between API handler and background workers.
    • Batched log shipping with buffer before flush.
    • Token bucket rate limiters for external API calls.

    Best practices

    • Size buffer based on measured producer/consumer rates.
    • Monitor channel len in metrics for queue depth alerting.
    • Use semaphore pattern for resource-bound concurrency.
    • Prefer small buffers — large buffers mask overload.
    • Combine with context timeout on blocked sends.

    Common mistakes

    • Huge buffer causing memory bloat under load spike.
    • Deadlock: all goroutines blocked on full channel, none receiving.
    • Assuming send always succeeds — blocks when full.
    • Using buffer to 'fix' race without proper sync.
    • Not handling shutdown when buffer has pending items.

    Advanced interview questions

    Q1Beginnerlen vs cap on channel?
    len is queued elements; cap is maximum buffer size.
    Q2BeginnerSemaphore with channel?
    chan struct{} buffer N — send acquires, receive releases slot.
    Q3IntermediateChoose buffer size?
    Based on throughput mismatch, memory budget, latency SLA — measure don't guess.
    Q4IntermediateNon-blocking channel operation?
    select with default case on send/receive.
    Q5AdvancedDesign backpressure for slow consumer.
    Bounded buffer + drop/reject policy when full; monitor depth; scale consumers.

    Summary

    Buffered channels queue up to cap elements before blocking. Use as semaphores to limit concurrent resource usage. Size buffers deliberately — oversized buffers hide overload. Monitor queue depth in production systems. Next lesson: select — multiplexing channel operations.

    Ready to mark this lesson complete?Track your journey across the entire course.