-
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
183: Whiteboard Practice: Implementing a Worker Pool from Scratch
Imagine you're running a busy coffee shop. If every single customer who walked through the door demanded their own personal barista, you'd run out of floor space and go bankrupt in ten minutes. Instead, you hire three experienced baristas. They stand behind the counter, and there's a single line of orders (the queue). As soon as a barista finishes one latte, they look at the line and grab the next order. This keeps your resource usage predictable and prevents the shop from collapsing under its own weight.
In Go, a worker pool is exactly that. The "orders" are data sent over a channel, the "baristas" are a fixed number of goroutines, and the "counter" is the channel itself. We do this because while goroutines are cheap, they aren't free. If you spawn 100,000 goroutines to process 100,000 API calls simultaneously, you'll likely hit rate limits, exhaust file descriptors, or blow through your memory. Throttling that concurrency is a mark of production-ready code.
Setting up the Conveyor Belt
First, we need a way to define what "work" is and a way to transport it. I usually start by defining a simple Job struct. We also need a channel to send these jobs to the workers. The key here is that the workers will all be listening to the same channel. Go's channel implementation ensures that each job is received by exactly one worker—no two baristas will try to make the same latte.
type Job struct {
ID int
Value int
}
func worker(id int, jobs <-chan Job, results chan<- int) {
for j := range jobs {
// Simulate some heavy lifting
fmt.Printf("Worker %d started job %d\n", id, j.ID)
time.Sleep(time.Millisecond * 100)
results <- j.Value * 2
}
}
Notice that I used range jobs. This is a clean way to tell the worker: "Keep taking jobs until the channel is closed." Once the channel closes, the loop terminates, and the goroutine exits gracefully.
Hiring the Baristas
Now we need to actually spawn the workers. I like to do this in a loop before I start sending any data. If we spawn them after sending data, we might fill the channel buffer and block our main thread before the workers even start. I've seen people make this mistake in interviews all the time—they forget that the "pool" needs to be ready and waiting for the work.
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan Job, numJobs)
results := make(chan int, numJobs)
// This is our "hiring" phase
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// Send the work
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Value: j}
}
close(jobs) // Tell workers no more jobs are coming
}
Knowing when the shift is over
Here is where most developers trip up: how do you know when all the workers are actually done? Just closing the jobs channel tells the workers to stop looking for new work, but they might still be processing their final task. If your main function exits too early, you'll lose that last bit of data.
I always reach for a sync.WaitGroup here. It's the most explicit way to say, "Wait for these specific N goroutines to signal they are finished." I wrap the worker logic so the WaitGroup can be decremented right before the goroutine dies.
func worker(id int, jobs <-chan Job, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done() // Mark this barista as 'off the clock'
for j := range jobs {
results <- j.Value * 2
}
}
// In main...
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// ... after sending jobs and closing the channel:
go func() {
wg.Wait()
close(results) // Now it's safe to close results
}()
By closing the results channel in a separate goroutine, we can range over the results in our main thread without deadlocking. It's a pattern you'll see in almost every high-performance Go project.
📋 Practical Task
Implement a Concurrent Image Metadata Processor
You are building a tool that processes a list of "image files" to extract metadata. Since reading files is I/O intensive, you need to implement a worker pool to avoid overwhelming the system.
Requirements:
- Create a
FileJobstruct that contains aFilename(string) and aFileSize(int). - Create a
Metadatastruct that contains theFilenameand aProcessedHash(string). - Implement a
processorworker function that:- Receives
FileJobfrom a jobs channel. - Simulates a delay using
time.Sleep(e.g., 50ms). - Generates a dummy hash (you can just return the filename + "_processed").
- Sends the
Metadataresult to a results channel.
- Receives
- In your
mainfunction:- Initialize a pool of 5 workers.
- Feed 20 dummy files into the jobs channel.
- Use a
sync.WaitGroupto ensure all workers finish before the program exits. - Print the final metadata results as they come in.
There are no comments for now.