Concurrency Fundamentals In Go: Understanding Go's Approach To Concurrency With Goroutines
Overview
This chapter introduces Go’s unique approach to concurrency, focusing on goroutines as lightweight, independent functions that enable efficient concurrent programming. You will explore how Go’s concurrency model encourages communication through channels rather than shared memory, making code faster, more robust, and easier to scale. The chapter also covers the differences between concurrency and parallelism, the role of Go’s scheduler, and how channels simplify communication between goroutines. Additionally, you’ll get a glimpse into the potential pitfalls of data races and Go’s powerful race detector.
Benefits Of Go's Concurrency Model
Due to the way in which Go’s concurrency model works, it’s common with just a few simple transformations to convert code that is:
- slow
- sequential
- failure-sensitive
to code that is:
- fast
- concurrent
- replicated
- robust
What Is Concurrency?
Concurrency is the composition of independently executing computations.
Concurrency is a way to structure software, particularly as a way to write clean code that interacts well with the real world.
Concurrency Is Not Parallelism
Concurrency is not parallelism, although it enables parallelism.
- Concurrency is about dealing with a lot of things at once.
- Parallelism is about doing a lot of things at once.
If you have only one processor, your program can still be concurrent but it cannot be parallel.
If you have a single resource, and multiple tasks, you can share your time across those tasks, thus work on all tasks concurrently. However, to work on all tasks in parallel, you need need more than one resource.
The Problem
Let’s say that I have a dog, and I have some treats, and I want to feed the dog some treats. The treats are kept in a box.
To feed my dog, I go to the box, I put a treat in my hand, then I walk to the dog and feed the dog it’s treat.

Adding Dogs
What happens, however, if I get another dog. Now when I feed them treats, I have a resource constraint of just one hand.

Now, to feed both dogs, I have to feed a treat to one dog, then take another treat from the box in my hand, and then feed a treat to the other dog.
It’s important to note now that because I can only feed one dog at a time, the other dog is waiting.
This is a concurrent operation.
More Hands
I can decide to use my other hand now as well, and feed both dogs at the same time. However, because there is only one box of treats, I can’t put both my hands in the box at the same time. This means that I have to schedule each hand’s timing to get a treat.

Even though I have enough hands to feed the dogs in parallel, I can still only do it concurrently as I am constrained by the single box of dog treats.
More Scenarios
if we add more treats and dogs, we will still only have two hands. While this may allow for a faster concurrent operation (less waiting to get the treats as there is less contention with scheduling), we still always have at least one dog waiting for a treat. This is one of many common design challenges that software engineers need to solve for when creating concurrent solutions

Goroutines
It is called a goroutine because existing terms, such as threads, coroutines,
processes, etc, convey inaccurate connotations.
Simply put, a goroutine is an independant function, launched by a go statement, capable of being run concurrently with other goroutines.
A goroutine is NOT a system thread.
…But if you think of it as a very cheap thread, you won’t be far off…
Goroutine Characteristics
- common to have hundreds, thousands, even millions of goroutines
- have their own call stack, which grows and shrinks as required
- they require only a few bytes of memory
- there might be only one thread in a program with thousands of goroutines.
The Go Scheduler
The scheduler in Go is responsible to distribute the runnable goroutines over multiple worker OS threads that run on one or more processors. It does this by
work-sharing and work-stealing.
Work-sharing: When a processor generates new threads, it attempts to migrate some of them to the other processors with the hopes of them being utilized by the idle/underutilized processors.Work-stealing: An underutilized processor actively looks for other processor’s threads and “steal” some.
For a more in-depth look at the scheduler, read Go’s work-stealing scheduler.
Channels
Channels are a great way to communicate in Go. One of the core tennants of Go’s concurrency model is:
Do not communicate by sharing memory; instead, share memory by communicating.
You can think of channels as pipes that connect concurrent goroutines. You can send values into
a channel from one goroutine, and receive those values in another goroutine.
Sync Package
The sync package contains many of the primitive building blocks needed to synchronize communication across goroutines. An in depth look at those primitives will be covered in the following chapters.
Key synchronization primitives include:
WaitGroup - coordinates waiting for a collection of goroutines to finish. Go 1.25 introduced
WaitGroup.Go(), which is now the preferred way to launch goroutines with a WaitGroup as it handles the bookkeeping automatically and reduces common bugs.Mutex - provides mutual exclusion to protect shared resources from concurrent access.
sync.Map - a concurrent-safe map implementation that can be used from multiple goroutines without additional locking. Go 1.23 added a
Clear()method for atomic cache invalidation.
Data Races
Race conditions are among some of the most difficult bugs to find and fix. It happens when one or more goroutine reads and writes to the same memory space.
Go ships with a very powerful race detector to help with the challenges around making your code safe. We will cover the race detector and how to use it in-depth in another chapter. However, if you want to learn more now, read the Introducing the Go Race Detector blog post.