4. Buffered Channels

Buffered Channels

Channels can optionally be buffered.

Creating a Channel With a Buffer

You can provide a buffer length as the second argument to make() to create a buffered channel:

ch := make(chan int, 100)

A buffer allows the channel to hold a fixed number of values before sending blocks. This means sending on a buffered channel only blocks when the buffer is full, and receiving blocks only when the buffer is empty.

Buffered Channels - Notes and Assignment

What Are Buffered Channels?

Unbuffered channel:

ch := make(chan int)  // No buffer
// Send blocks immediately until someone receives

Buffered channel:

ch := make(chan int, 100)  // Buffer of 100
// Send only blocks when buffer is full

Creating a Buffered Channel

Examples:

How Buffering Works

Think of it like a queue with limited space:

Buffer size = 3

Empty: [ | | ] ← Can send 3 times without blocking 1 item: [ X | | ] ← Can send 2 more 2 items: [ X | X | ] ← Can send 1 more Full: [ X | X | X ] ← Send blocks until someone receives

Blocking Behavior

Unbuffered (size 0)

Buffered

Assignment

We want to be able to send emails in batches. A writing goroutine will write an entire batch of email messages to a buffered channel, and later, once the channel is full, a reading goroutine will read all of the messages from the channel and send them out to our clients.

Complete the addEmailsToQueue function. It should create a buffered channel with a buffer large enough to store all of the emails it's given. It should then write the emails to the channel in order, and finally return the channel.

Assignment Solution

Solution Breakdown

1

Create Buffered Channel

  • Buffer size = number of emails

  • This ensures we can add all emails without blocking

2

Write All Emails

  • Loops through all emails

  • Sends each one into the channel

  • Doesn't block because buffer is big enough

3

Return Channel

  • Returns the channel full of emails for the reader to process later.

How It Will Be Used

Example Trace

Execution:

  1. Create channel with buffer size 3

  2. Send "a@example.com" β†’ buffer: [a@example.com]

  3. Send "b@example.com" β†’ buffer: [a@example.com, b@example.com]

  4. Send "c@example.com" β†’ buffer: [a@example.com, b@example.com, c@example.com]

  5. Return channel

No blocking because buffer fits all 3 emails!

Why Use Buffered Channels?

Batch Processing:

Prevents Blocking:

Buffered vs Unbuffered

Type
Creation
Send Blocks When
Receive Blocks When

Unbuffered

make(chan int)

Immediately

Channel empty

Buffered

make(chan int,5)

Buffer full

Channel empty

Common Patterns

Pattern: Exact Size Buffer

Pattern: Worker Queue

Pattern: Rate Limiting

Key Takeaways

  1. Buffer syntax: make(chan Type, size)

  2. Send blocks when buffer is full

  3. Receive blocks when buffer is empty

  4. Use buffered for batch processing

  5. Buffer size = len(data) for non-blocking writes

Quick Reference

Challenge

Buffered Log Collector

Complete the collectLogs function. It should use a buffered channel and a goroutine to send and receive log messages.

You are building a tiny log system for a game server. Logs arrive as a slice of strings. You want to process them using a buffered channel so the sender is not blocked on every single message.

Implement:

It should:

1

Create a buffered channel of strings with capacity bufferSize.

2

Start a goroutine that:

  • Sends every message from messages into the channel.

  • Closes the channel after all messages are sent.

3

On the main goroutine (inside collectLogs):

  • Receive from the channel until it is closed.

  • Collect all received messages in order into a result slice.

4

Return the result slice. The order of messages must be the same as the input order.

Solution

Key Steps

  1. Create buffered channel β€” make(chan string, bufferSize) with specified capacity

  2. Spawn goroutine β€” sends all messages concurrently

  3. Close channel β€” signals no more messages coming

  4. Range over channel β€” receives until closed

  5. Build result slice β€” append each message in order

  6. Return slice β€” contains all messages in original order

Important Concepts

  • Buffered Channel: make(chan string, bufferSize) can hold bufferSize messages before blocking.

  • close(channel): signals no more values will be sent and allows range to exit.

  • range over channel: receives values until channel is closed; preserves FIFO order.

Common Mistakes Avoided

  • ❌ Using len(messages) instead of the bufferSize parameter

  • ❌ Not using a goroutine to send

  • ❌ Forgetting to close the channel

  • ❌ Returning channel instead of slice

  • ❌ Not receiving from the channel

Why the goroutine must call close() inside it

If close() is called outside the goroutine immediately after starting it, the channel may be closed before the goroutine sends all messages, causing sends on a closed channel (panic). Closing inside the goroutine ensures it's closed only after all sends complete.

Why use go with an immediately-invoked anonymous function

  • go func() { ... }() defines and calls an anonymous function as a goroutine.

  • Running the send loop in a goroutine allows sending and receiving to proceed concurrently, preventing deadlock when the buffer is smaller than the number of messages.

Flow Diagram (logical)

Main Goroutine
Worker Goroutine

Create channel

Start goroutine ───────────

β†’ Start sending messages

Continue immediately

mess[0] β†’ channel

Start receiving loop

mess[1] β†’ channel

Receive mess[0] ←──────────

←

Receive mess[1] ←──────────

←

Range loop exits | Goroutine ends

Both sending and receiving happen concurrently, preserving order and avoiding deadlock.