Other | Apr 29, 2025 | 6 min read
In this blog, we explore how Go’s concurrency model helps us do more with less. Through a fun kitchen party analogy, you’ll learn about goroutines, channels, waitgroups, semaphores, and how to use them together to build efficient, concurrent programs.
We want everything fast. Groceries in minutes. Replies in seconds. A date in a few swipes.
We don’t like to wait anymore. So we build software that keeps up. Software that tries to deliver your output as quickly as possible.
But speed doesn’t always mean throwing more resources at the problem. Often, it means using what we have more efficiently.
That’s where concurrency comes in. It’s one of the many tools that help us do more with less.
In this blog, I’ll try to explain concurrency, goroutines, and the complete Go toolkit for working with concurrent code again (the best explanation will always be this 13-year-old video by Rob Pike). This time by throwing a party ( the second best explanation I think I’ve found on r/golang subreddit).
Need real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleNeed real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleIn ELI5 terms, concurrency is about dealing with multiple things at once. Like your or my brain, we’re always DEALING with multiple things.
Concurrent thoughts of a Developer Advocate
In software development, it means your system can handle multiple functions or processes at one time. In Go, this doesn't mean threads or processes like in other languages.
Go allows you to implement concurrency with the help of goroutines and gives you a toolkit for working with concurrent code.
Let’s understand the toolkit in more depth.
Instead of rewriting the same definition of goroutines again, let’s see that Reddit post I was talking about.
Reddit analogy post
Remember, the Airbnb house is your Go program.
The friends are your goroutines.
Each kitchen is a CPU core (or logical thread).
The party is the deadline (a goal your program is racing toward).
Each dish (task) is cooked in its own kitchen. You start a goroutine for each meal.
In Go, you start a goroutine using the keyword go
:
go bakeLemonCake(...)
go bakeStrawberryCupcakes(...)
go grillChicken(...)
go cookGoatStew(...)
You don’t wait for one to finish before starting another. That’s the core of Go’s concurrency model.
None of the friends knows what the right amount of sugar is that goes in the desserts. So once the lemon cake group figures it out, they send the details about the levels over to another group.
sugarLevelChan <- sugarLevel // Sender
data := <-sugarLevelChan // Receiver
Remember, they are not sharing sugar directly. They’re sending information about the sugar level. That’s what channels are for.
Go shares memories by communicating.
What if the cupcake team is still busy, but the lemon cake team has already measured the sugar? Instead of waiting for a reply, they leave a sticky note with the value.
In Go code, this translates to:
bufferedChan := make(chan int, 2)
bufferedChan <- 1
bufferedChan <- 2
fmt.Println(<-bufferedChan)
fmt.Println(<-bufferedChan)
That’s a buffered channel. A little message queue. The buffer holds values temporarily.
You want to serve food only after all dishes are ready. So you ask everyone to send a thumbs-up when they’re done. That’s the function of Waitgroups.
var wg sync.WaitGroup
wg.Add(4)
go func() { bakeLemonCake(); wg.Done() }()
go func() { bakeStrawberryCupcakes(); wg.Done() }()
go func() { grillChicken(); wg.Done() }()
go func() { cookGoatStew(); wg.Done() }()
wg.Wait()
Add()
tasks, then Wait()
for them to complete.
The house has only two ovens shared by all four kitchens.
Even though each team has its own kitchen, they have to take turns using the oven. But only two teams can use the ovens at once.
You need a way to limit that.
That’s where semaphores come in.
In Go, you'd use a channel as a counting semaphore:
// useOven demonstrates the use of a semaphore (ovenSlots) to limit
// concurrent oven usage
func useOven(dish string, ovenSlots chan struct{}) {
ovenSlots <- struct{}{} // acquire (semaphore pattern)
fmt.Println("Using oven for", dish)
time.Sleep(2 * time.Second) // simulate oven time
<-ovenSlots // release (semaphore pattern)
fmt.Println("Done with", dish)
}
Each team calls useOven()
to wait for an available slot.
// bakeLemonCake demonstrates sending data through a channel and using a WaitGroup
func bakeLemonCake(sugarLevelChan chan<- int, wg *sync.WaitGroup, ovenSlots chan struct{}) {
defer wg.Done()
fmt.Println("Baking lemon cake: Deciding sugar level...")
time.Sleep(1 * time.Second) // Simulate time to decide
sugarLevel := 5
fmt.Printf("Baking lemon cake: Sugar level decided: %d\\n", sugarLevel)
sugarLevelChan <- sugarLevel // Send sugar level to channel
useOven("lemon cake", ovenSlots)
fmt.Println("Baking lemon cake: Done!")
}
Even if four teams start concurrently, only two can use the ovens at a time.
The others wait.
Now you’re waiting to hear back from the chicken team or the stew team. Whoever finishes first gets served.
// Wait for either chicken or stew to finish first, or timeout after 5 seconds
select {
case dish := <-chickenDone:
fmt.Println(dish, "team finished first and gets served!")
case dish := <-stewDone:
fmt.Println(dish, "team finished first and gets served!")
case <-time.After(5 * time.Second):
fmt.Println("Timeout: No dish finished in time!")
}
You respond to whichever group pings you first.
You don’t wait in line. You respond to the one that’s ready first. That’s what the select statement
helps with.
Congratulations. You’ve just thrown a concurrent party in Go. You called your friends (goroutines), passed messages (channels), took turns using the oven (semaphores), and waited for everyone to finish before serving (WaitGroups).
We learned five core ideas behind Go concurrency through this example:
You can find the complete code on GitHub.
None of these are abstract patterns. They’re heavily used when writing backend systems, CLI tools, or concurrent services.
Go doesn’t hide concurrency behind frameworks. It gives you simple, composable tools. With just goroutines, channels, and a few sync primitives, you can model everything from a kitchen party to a production-grade microservice.
And that's the real power of Go: it lets you reason about concurrency.
Don't reach for a bigger machine the next time you face a performance bottleneck.
Reach for a goroutine. And maybe a spatula.
For a more complex implementation of Go’s concurrency, check out Treblle’s Go SDK and contribute to the SDK if you can.
Need real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleNeed real-time insight into how your APIs are used and performing?
Treblle helps you monitor, debug, and optimize every API request.
Explore TreblleOn June 26, 2025, we hosted a webinar with API security expert Colin Domoney and Treblle’s Vedran Cindrić to unpack what really breaks API security. Here are the key takeaways, including real breach examples, common myths, and a practical security checklist.
Missed the webinar? Here are the top takeaways from “The Future Is Federated,” where Daniel Kocot, Vedran Cindrić, and Harsha Chelle shared practical strategies for scaling API governance in complex, fast-moving environments.
APIs aren’t just connectors, they’re products with real users. To succeed, teams must understand those users deeply. This article explores why consumer insight is the key to building better APIs and how leaders can turn that understanding into action.