Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
31: Buffered vs Unbuffered Channels
Up until now, we've been using unbuffered channels. In Go, when you call make(chan T), you're creating a synchronous handoff. I like to think of it as a relay race: the runner with the baton cannot let go until the next runner has their hand firmly on it. If the receiver isn't there, the sender just stands there, frozen.
Setting up a synchronous handoff
Let's build a simple image processing pipeline. We have a "Dispatcher" that sends filenames to a "Worker" that simulates a heavy resize operation. I'll start with an unbuffered channel because it's the safest default—it guarantees that the worker has actually received the task before the dispatcher moves on.
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan string) {
for job := range jobs {
fmt.Printf("Worker %d processing %s\n", id, job)
time.Sleep(time.Millisecond * 500) // Simulate work
}
}
func main() {
jobs := make(chan string) // Unbuffered
go worker(1, jobs)
for _, img := range []string{"photo1.jpg", "photo2.jpg", "photo3.jpg"} {
fmt.Printf("Dispatching %s...\n", img)
jobs <- img
fmt.Printf("Finished dispatching %s\n", img)
}
close(jobs)
}
If you run this, you'll notice a rhythmic "stop-and-go" pattern. The dispatcher says "Dispatching photo1.jpg...", then it stops. It doesn't print "Finished dispatching" until the worker actually pulls the value off the channel. It's a tight coupling.
The bottleneck of strict synchronization
Now, imagine our dispatcher is actually an HTTP handler receiving uploads. We don't want the user's request to hang just because the worker is currently busy resizing a different image. We want to "drop the task in the inbox" and tell the user "Got it!" immediately.
Here is where I made a mistake in a production service a few years back. I tried to solve this by just spawning a new goroutine for every single send: go func() { jobs <- img }(). I thought I was being clever by making it asynchronous, but I actually just created a memory leak. If the worker was slow, I was just piling up thousands of dormant goroutines in the background, each waiting for their turn to send to the channel.
The correct way to decouple the sender from the receiver—without spawning a million goroutines—is a buffered channel.
Adding a cushion with buffering
By adding a capacity to make(chan T, capacity), we create a buffer. Now, the dispatcher can keep sending images as long as the buffer isn't full. It's like a mailbox; the mailman can leave the letter even if you aren't home to take it.
func main() {
// Now we have a buffer of 3.
// The dispatcher can send 3 images without the worker even touching them.
jobs := make(chan string, 3)
go worker(1, jobs)
for _, img := range []string{"photo1.jpg", "photo2.jpg", "photo3.jpg", "photo4.jpg"} {
fmt.Printf("Dispatching %s...\n", img)
jobs <- img
fmt.Printf("Finished dispatching %s\n", img)
}
close(jobs)
}
Run this, and the output changes. You'll see "Finished dispatching" for the first three images almost instantly. The dispatcher only blocks when it hits "photo4.jpg", because the buffer is full and it has to wait for the worker to clear a spot.
Knowing when to stop buffering
You might be tempted to just give your channels a massive buffer—like 10,000—to "ensure it never blocks." Resist that urge. A buffer is a tool for smoothing out bursty traffic, not a replacement for a proper queue.
If your buffer is always full, you don't have a concurrency problem; you have a throughput problem. Your worker is too slow. Increasing the buffer just hides the latency and consumes more memory until the program eventually crashes or lags. Use buffered channels when you have a known, reasonable burst size, but stick to unbuffered channels when you need a strong guarantee that the data has been handed off.
📋 Practical Task
Build a Burst-Resistant Log Batcher
Create a program that simulates a high-frequency logging system. Your goal is to ensure the main application doesn't freeze during a "log burst."
- Create a
logChanwith a buffer size of 5. - Implement a
loggergoroutine that reads from the channel and prints"Writing to disk: [message]". To simulate disk I/O, add atime.Sleepof 200ms inside the logger. - In the
mainfunction, use a loop to send 10 log messages to the channel as quickly as possible. - Print a timestamp or a message
"Log sent!"immediately after each send.
Verification: If your buffer is working, you should see the first 5-6 "Log sent!" messages appear almost instantly, while the "Writing to disk" messages appear slowly one by one. The remaining logs should only be sent once the logger creates space in the buffer.
There are no comments for now.