Mahdi.Kh
September 14, 2026
We get familiar with the sync standard package and look at the parts of it that are used most often.
The sync package in Go is one of the most important standard packages for synchronization between multiple goroutines.
Before using the sync package, it's best to get familiar with the concept of a race condition.
Technically speaking, a race condition is a situation in which multiple execution flows (threads, goroutines, or processes) access a shared resource, and the final outcome of the program depends on the order or timing in which they run. In other words, if the execution order of these flows changes for any reason, the program's behavior may change as well.
The important point is that a race condition isn't just about things "running at the same time"; it's about the lack of synchronization between operations on a shared resource.
For a race condition to occur, three conditions usually need to hold:
If all three conditions hold, the program's outcome can become unpredictable!
Because the execution flows are, in a sense, in a race to reach the shared resource first and carry out their operation. The winner of the race can be different each time, so the program's outcome can also differ from one run to the next.
Race conditions come in different types, which can be categorized as follows:
Next, we'll briefly go over each of these types of race conditions.
One of the most common forms of race condition is the data race.
The result is that the final value of the variable is undetermined!
For example, if you run the code below several times, you may get a different result each time:
When multiple goroutines work on the same shared data or resource at the same time, you may run into problems such as a data race!
The concepts of data race and race condition are often used interchangeably, but incorrectly so. Every data race is a race condition, but not every race condition is necessarily a data race.
In Go, the go run -race tool is designed specifically to catch this kind of race! You just need to use the -race flag when running or testing your program so that extra checks are performed, and you're notified if a race occurs.
In this case, the program first checks a condition and then makes a decision based on it. The problem is that between checking the condition and performing the action, the state of the data may change. As a result, the decision that was made is no longer valid.
In this type, the program assumes operations must happen in a particular order, but there is no guarantee of that order. As a result, an operation may run before the data is ready, or dependencies may not be respected.
We can resolve race conditions using the sync package. Next, we'll briefly go over each of the following parts of the sync package:
sync.WaitGroupThe main job of sync.WaitGroup is to make one goroutine wait until several other goroutines have finished running. In other words, sync.WaitGroup provides a mechanism for telling a goroutine when a set of other goroutines have completed their work.
sync.WaitGroup:Whenever we need to wait for goroutines to finish their work, we can use sync.WaitGroup. Inside sync.WaitGroup there is a counter. To increase the counter, we call the Add method before running each goroutine, and inside each goroutine we decrease the counter by calling the Done method, which signals that that goroutine's work is finished.
Where we need to wait for the goroutines to finish, we use the Wait method. This method blocks, preventing the next lines of code from running, until the counter reaches zero.
In Go version 1.26, the Go method was added to sync.WaitGroup. Depending on the situation, you can decide to use the Add and Done methods, or use the Go method, which calls Add and Done for you automatically.
Using the Go method, we can rewrite the code above as follows:
sync.Mutex and sync.RWMutexMutex is short for "Mutual Exclusion," and it's one of the important tools in concurrent programming for synchronization.
By creating a locking mechanism, a mutex guarantees that at any given moment only one thread or goroutine can access a shared resource or data. This prevents concurrent, inconsistent access to the data and avoids problems such as race conditions and corrupted data state.
This means that when multiple goroutines access shared data, a Mutex or RWMutex can be used to control access to that data and prevent concurrent modification. Depending on the type of lock, either only one goroutine is allowed to access the data at any given moment (Mutex), or multiple goroutines can read the data at the same time while only write operations are done exclusively (RWMutex).
sync.Mutex and sync.RWMutex are used to protect shared data in a concurrent environment; we do this to prevent data races.
The main difference between Mutex and RWMutex is the kind of access they allow goroutines:
sync.Mutex: At any given moment, only one goroutine can enter the protected section (critical section). It doesn't matter whether the access is for reading or writing; other goroutines must wait until the lock is released.
sync.RWMutex: Distinguishes between read access and write access. Multiple goroutines can read the data at the same time, but when a goroutine wants to change the data, it's given exclusive access, and other readers and writers must wait until the write operation finishes.
In the earlier examples, the program's result could differ on each run, and the output might not be calculated correctly because of the data race (the sum of 10 and 1000 should be 1010). We can rewrite the earlier examples as follows to fix the problem:
In the example above, by using sync.Mutex at any given time, we give exclusive access to x (both writing and reading) to a single goroutine. This way, multiple goroutines can't access x at the same time. Each goroutine calls Lock when it starts its work, and this call blocks. Whichever goroutine acquires the lock first causes the rest of the goroutines to wait at the lock.Lock() line. Whenever the goroutine holding the lock releases it, one of the waiting goroutines can acquire it, and this way, a shared lock ensures that only one goroutine can access x at any given time.
sync.OnceSometimes we need a specific operation to run only once during a program's execution, regardless of how many times it's called. For this purpose, we can use sync.Once. It guarantees that a specific function or operation runs only once, even if multiple goroutines try to call it at the same time!
Every variable of type sync.Once makes sure the function we pass to the Do method runs only once. If we later call Do again with the same variable, that function will no longer run, and later calls are ignored.
Previous part: The time Standard Package | Go in Plain Language
Next part: Introducing go fix | Go in Plain Language