
Concurrency in Go: My Learning Journey
I came across Go's concurrency model while working on a microservice where we needed to do some operations which were non-blocking in nature and could be done independently. If done in sync, they would have increased the API latency and might led to timeouts.
I started digging into how Go handles concurrency, and what I found was refreshingly simple. Over time, I began to appreciate just how clean and practical Go’s approach really is.
Understanding Concurrency (Not Just Parallelism)
One of the early realizations I had was that concurrency isn’t the same as parallelism. Parallelism is about doing things at the exact same time (think multi-core execution), while concurrency is more about managing multiple things at once.
It’s like managing a kitchen during dinner rush. We’re not cooking all dishes at the same moment, but we are managing all the orders, moving between tasks, and coordinating their completion.
Go makes this kind of orchestration incredibly approachable. At the heart of it all are Goroutines and Channels, which work together in a surprisingly elegant way.
First Time I Launched a Goroutine
go sendEmail()
This would result the function sendEmail() to run async
Full code -
package main import ( "time" ) func sendEmail() { ....//do some operation to send email } func main() { doSomeWork() go sendEmail() time.Sleep(time.Second) // Give the Goroutine time to finish }
sendEmail() - these weren’t just threads, they were incredibly lightweight and easy to spawn. I suddenly saw the potential - I could launch dozens, even hundreds of tasks, with barely any friction.
Communicating with Channels
Of course, concurrency isn’t just about running things in parallel . It’s about coordinating them. That’s where Channels come in.
One of my early issues came from trying to share state directly between Goroutines using a regular variable. It worked in development but broke under load. That’s when I understood why channels matter.
They are built for Goroutines to talk to each other "safely". (*safely is important)
Example snippet -
func square(n int, ch chan int) { ch <- n * n } func main() { ch := make(chan int) go square(4, ch) result := <-ch fmt.Println("Square:", result) }
WaitGroups: Synchronization Made Simple
As our use of Goroutines expanded, I ran into situations where I needed to wait for a bunch of them to finish before continuing. That’s when I found sync.WaitGroup
, and it felt like exactly the tool I needed.
Example usage -
var wg sync.WaitGroup func worker(id int) { defer wg.Done() fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { for i := 1; i <= 5; i++ { wg.Add(1) go worker(i) } wg.Wait() }
It is straightforward, readable, and surprisingly powerful.
The idea that we can coordinate Goroutines without complex thread management is indeed no less than a boon.
Buffered vs. Unbuffered Channels
At some point, I ran into deadlocks. That’s how I learned the difference between buffered and unbuffered channels. After some long google searches but yeah finally.
- Unbuffered channels block until both the sender and receiver are ready.
- Buffered channels, on the other hand, let us decouple the send and receive to some extent.
ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch)
It's like a juggling.
Select: A Simple, Beautiful Multiplexer
Later on, I had a use case where I needed to wait for whichever channel was ready first. select was go to thing for this purpose.
select { case msg1 := <-ch1: fmt.Println("Received from ch1:", msg1) case msg2 := <-ch2: fmt.Println("Received from ch2:", msg2) case <-time.After(time.Second): fmt.Println("Timeout") }
Instead of nesting multiple if-else or switch statements, I could wait for any of multiple operations, with a fallback for timeout.
Final thoughts
Goroutines and Channels encourages us to think in terms of cooperation, not independent parallel processes. And all these can be achieved with a bare minimum syntax. There lies the power of minimalism - simple yet powerful !