> Какие примитивы синхронизации есть в стандартной библиотеке Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Lamoda

Стек: Go

> Пример ответа

Короткий ответ

В стандартной библиотеке Go есть три основных примитива синхронизации: sync.Mutex (и sync.RWMutex), sync.WaitGroup и sync.Once. Также к ним относятся sync.Cond, sync.Pool и sync.Map, хотя последние два - это скорее высокоуровневые структуры, а не чистые примитивы. Каналы (chan) тоже можно считать примитивом синхронизации, так как они обеспечивают передачу данных и синхронизацию между горутинами.

Подробное объяснение

Все примитивы синхронизации в Go находятся в пакете sync. Основные из них:

  • sync.Mutex - взаимное исключение для защиты критических секций. Методы Lock() и Unlock().
  • sync.RWMutex - mutex с разделением на чтение и запись. Позволяет множеству читателей одновременно, но только одному писателю. Методы RLock(), RUnlock(), Lock(), Unlock().
  • sync.WaitGroup - ожидание завершения группы горутин. Методы Add(delta), Done(), Wait().
  • sync.Once - гарантирует, что функция выполнится ровно один раз, даже при конкурентном вызове. Метод Do(f).
  • sync.Cond - условная переменная для сигнализации между горутинами. Методы Wait(), Signal(), Broadcast().
  • sync.Pool - пул временных объектов для переиспользования и снижения нагрузки на GC.
  • sync.Map - конкурентно-безопасная map, оптимизированная для сценариев с разными паттернами доступа.

Каналы (chan) - отдельный механизм, встроенный в язык. Они поддерживают синхронизацию через операции отправки и получения. Буферизованные каналы позволяют передавать данные без блокировки до заполнения буфера, небуферизованные - синхронизируют отправителя и получателя.

Важный нюанс: sync.Mutex и другие примитивы из sync нельзя копировать после первого использования - это приведёт к неопределённому поведению. Компилятор Go (начиная с версии 1.20) может предупреждать об этом через go vet.

На практике

Выбор примитива зависит от сценария:

  • Для защиты общих данных от гонок - sync.Mutex или sync.RWMutex (если чтений значительно больше, чем записей).
  • Для ожидания завершения параллельных задач - sync.WaitGroup.
  • Для инициализации singleton или ленивой загрузки - sync.Once.
  • Для координации по условию (например, producer-consumer с ожиданием данных) - sync.Cond или каналы.
  • Для кэширования и переиспользования объектов - sync.Pool.
  • Для конкурентного доступа к map с частыми чтениями и редкими записями - sync.Map (но в большинстве случаев лучше использовать обычную map + mutex).

Каналы предпочтительнее, когда нужно передавать данные между горутинами, а не просто синхронизировать доступ к памяти. Идиоматичный подход в Go - "не общайтесь через разделяемую память, разделяйте память через общение". Однако для простой защиты счётчика или флага mutex - более прямое и эффективное решение.

Пример кода

GO
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("counter:", counter)
}

Как отвечать на собеседовании

Начни с перечисления основных примитивов, затем кратко опиши назначение каждого. Упомяни, что каналы - это отдельный механизм, но тоже примитив синхронизации. Приведи пример использования sync.Mutex и sync.WaitGroup - это база, которую ожидают услышать. Если спросят про sync.Once или sync.Cond - объясни, когда они применяются. Не углубляйся в детали реализации, если не просят. Хорошо показать понимание trade-off между mutex и каналами: mutex - для защиты данных, каналы - для передачи данных и сигнализации.

Что проверяет интервьюер

Интервьюер оценивает:

  • знание стандартной библиотеки и её возможностей;
  • понимание различий между примитивами и сценариев их применения;
  • умение выбирать правильный инструмент под задачу;
  • понимание идиоматичного подхода Go к конкурентности;
  • знание ограничений (например, невозможность копирования mutex, опасность deadlock).

Типичные ошибки

  • Забывают упомянуть sync.RWMutex или sync.Once.
  • Путают sync.WaitGroup с sync.Once - первое для ожидания группы, второе для однократного выполнения.
  • Считают, что каналы - это не примитив синхронизации, а только средство передачи данных.
  • Не знают про sync.Cond и sync.Pool.
  • Предлагают использовать sync.Map там, где достаточно обычной map с mutex - это часто избыточно.
  • Не упоминают, что mutex нельзя копировать после использования.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью