В Go для синхронизации конкурентных горутин используются структуры из пакета sync, наиболее частые — sync.Mutex, sync.RWMutex и sync.WaitGroup.
sync.Mutex предоставляет механизмы взаимного исключения при доступе к разделяемым данным. Его методы — Lock() (блокирует) и Unlock() (разблокирует).
sync.RWMutex расширяет возможности обычного мьютекса: допускает параллельное чтение, но эксклюзивное изменение.
sync.WaitGroup предназначен для ожидания завершения группы горутин. С помощью Add(int), Done() и Wait() вы управляете жизненным циклом параллельной работы.
Например:
var mu sync.RWMutex data := 0 // Чтение mu.RLock() fmt.Println(data) mu.RUnlock() // Запись mu.Lock() data = 42 mu.Unlock()
Тонкости:
Unlock в блоке defer, чтобы не забыть разблокировать мьютекс даже при панике.WaitGroup.Done() соответствует Add().Можно ли захватить один и тот же sync.Mutex (или RWMutex) дважды подряд в одной и той же горутине? Что произойдёт?
Ответ: Нет, если вы вызовете Lock() на одном и том же Mutex дважды подряд без промежуточного Unlock(), горутина заблокируется навсегда (deadlock). В Go мьютексы нерекурсивные.
Пример:
var mu sync.Mutex mu.Lock() // ... mu.Lock() // DEADLOCK: заблокируемся навсегда, т.к. тот же поток уже держит замок
История
В проекте для high-load REST API разработчик обернул всю обработку запроса одним мьютексом. Это вызвало резкое падение производительности — только один запрос мог обрабатываться одновременно, хотя планировалось обслуживание тысяч клиентов. Причина — незнание разницы между Mutex и RWMutex и игнорирование параллельных чтений.
История
При копировании структуры с Mutex внутри одним из участников команды, случайно передали копию в другую функцию. Это привело к паническом сообщению "sync: copy of sync.Mutex" и крэшам в проде под высокой нагрузкой.
История
При использовании WaitGroup забыли вызвать Done() в нескольких горутинах, что привело к вечному ожиданию Wait(), блокирующему основной поток. В результате сервис терял доступность до ручной перезагрузки.