In Go, the structures from the sync package are used for synchronizing concurrent goroutines, the most common ones being sync.Mutex, sync.RWMutex, and sync.WaitGroup.
sync.Mutex provides mutual exclusion mechanisms when accessing shared data. Its methods are Lock() (locks) and Unlock() (unlocks).
sync.RWMutex extends the capabilities of a regular mutex: allowing parallel reads, but exclusive writes.
sync.WaitGroup is intended for waiting for a group of goroutines to finish. Using Add(int), Done(), and Wait(), you control the lifecycle of parallel work.
For example:
var mu sync.RWMutex data := 0 // Reading mu.RLock() fmt.Println(data) mu.RUnlock() // Writing mu.Lock() data = 42 mu.Unlock()
Nuances:
Unlock in a defer block to ensure the mutex is unlocked even during a panic.WaitGroup.Done() matches Add().Can the same sync.Mutex (or RWMutex) be acquired twice in a row in the same goroutine? What happens?
Answer: No, if you call Lock() on the same Mutex twice in a row without an intermediate Unlock(), the goroutine will deadlock forever. In Go, mutexes are non-recursive.
Example:
var mu sync.Mutex mu.Lock() // ... mu.Lock() // DEADLOCK: will block forever, as the same thread already holds the lock
Story
In a project for a high-load REST API, a developer wrapped the entire request handling in a single mutex. This caused a sharp decline in performance — only one request could be processed at a time, whereas thousands of clients were expected. The reason was ignorance of the difference between Mutex and RWMutex and neglecting parallel reads.
Story
While copying a structure with a Mutex inside by one team member, a copy was accidentally passed to another function. This led to a panic message "sync: copy of sync.Mutex" and crashes in production under heavy load.
Story
In using WaitGroup, failed to call Done() in several goroutines, leading to eternal waiting on Wait(), blocking the main thread. As a result, the service lost availability until a manual restart.