> Какие примитивы синхронизации существуют кроме каналов в Go (Go)

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

Компании: Wildberries, Black Wall Group (BWG), Фабрика решений

Стек: Go

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

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

В Go помимо каналов есть примитивы из пакета sync: Mutex, RWMutex, WaitGroup, Once, Cond, Pool, Map, а также атомарные операции из sync/atomic. Они покрывают сценарии, где каналы избыточны или неэффективны: защита разделяемых данных, однократная инициализация, ожидание завершения группы горутин, условная синхронизация. Выбор зависит от задачи - каналы лучше для передачи данных и композиции, примитивы - для низкоуровневого контроля состояния.

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

sync.Mutex - классический взаимный исключающий замок. Используется для защиты критических секций, когда несколько горутин читают и пишут общие данные. В отличие от каналов, не навязывает поток данных - просто блокирует доступ.

sync.RWMutex - расширение Mutex с раздельными блокировками на чтение и запись. Позволяет множеству читателей одновременно, но писатель получает эксклюзивный доступ. Эффективен для сценариев read-heavy.

sync.WaitGroup - счётчик ожидания завершения горутин. Основной паттерн: Add(n) перед запуском, Done() в конце каждой горутины, Wait() для блокировки до обнуления счётчика. Не заменяет каналы, но решает задачу "дождаться всех" без передачи данных.

sync.Once - гарантирует однократное выполнение функции, даже при конкурентном вызове. Используется для ленивой инициализации синглтонов, настройки глобального состояния.

sync.Cond - условная переменная. Позволяет горутине ждать наступления условия, а другой горутине - сигнализировать о его изменении (Signal, Broadcast). Требует внешнего Mutex и аккуратной работы с циклом проверки условия. Низкоуровневый примитив, часто заменяется каналами с close или буфером.

sync.Pool - пул временных объектов для переиспользования и снижения нагрузки на GC. Не гарантирует сохранность объектов - они могут быть удалены в любой момент. Полезен для тяжёлых аллокаций (буферы, соединения).

sync.Map - конкурентно-безопасная map, оптимизированная для сценариев, где ключи стабильны, а чтения преобладают. Внутри использует sharding и атомарные операции. Не заменяет обычную map с Mutex во всех случаях - для записи и смешанных нагрузок часто проще классика.

sync/atomic - атомарные операции над числами и указателями: AddInt64, CompareAndSwap, Load, Store. Позволяют реализовать lock-free структуры и счётчики без блокировок. Требуют выравнивания памяти и осторожности - ошибки приводят к data race, которые не всегда ловятся детектором.

Отдельно стоит упомянуть runtime.Gosched и runtime.GOMAXPROCS - это не примитивы синхронизации, но влияют на планировщик. Также есть context.Context - не примитив, но используется для отмены и дедлайнов в связке с каналами.

На практике

Выбор между каналами и примитивами - это trade-off между выразительностью и контролем. Каналы лучше для:

  • передачи данных между горутинами;
  • паттернов fan-in/fan-out, pipeline;
  • сигнализации о событии через close.

Примитивы - для:

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

Практическое правило: если задача - "передать значение" - канал. Если "защитить состояние" - Mutex или atomic. Не смешивайте без необходимости: каналы внутри мьютекса - плохой тон, как и мьютекс внутри канала.

sync.Cond на практике редок - почти всегда можно выразить через канал с буфером 1 или close. sync.Pool требует осторожности: объекты не должны хранить состояние между использованиями.

Пример кода

GO
// Защита счётчика через Mutex
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
// Атомарный счётчик
type AtomicCounter struct {
n int64
}
func (c *AtomicCounter) Inc() {
atomic.AddInt64(&c.n, 1)
}
func (c *AtomicCounter) Value() int64 {
return atomic.LoadInt64(&c.n)
}
// WaitGroup для ожидания группы
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// работа
}(i)
}
wg.Wait()
// Once для ленивой инициализации
var (
once sync.Once
cfg *Config
)
func GetConfig() *Config {
once.Do(func() {
cfg = loadConfig()
})
return cfg
}

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

Начните с перечисления примитивов из sync и sync/atomic. Затем кратко опишите назначение каждого. Подчеркните, что каналы - это примитив для коммуникации, а остальные - для синхронизации состояния. Приведите пример выбора: для счётчика - atomic, для map - RWMutex, для ожидания горутин - WaitGroup.

Если спросят про Cond - объясните, когда он нужен (сложное условие, множественные ожидающие), но честно скажите, что на практике часто заменяется каналами. Упомяните sync.Pool и sync.Map как специализированные инструменты.

Хорошо показать понимание trade-off: каналы дают композицию и безопасность, но медленнее и тяжелее. Мьютексы - быстрее, но требуют дисциплины. Атомика - максимальная скорость, но сложность и ограничения.

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

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

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

Для senior важно показать, что вы не просто заучили список, а понимаете внутреннее устройство: как Mutex работает через futex, почему atomic требует выравнивания, как WaitGroup реализован через семафор. Также проверяется умение рассуждать о производительности и конкурентности.

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

  • Утверждение, что каналы - единственный способ синхронизации. Это не так, и интервьюер это сразу заметит.
  • Путаница между Mutex и RWMutex: использование RWMutex там, где чтения редки, или наоборот.
  • Забывают про sync.Once - классический вопрос про синглтон.
  • Неправильное использование WaitGroup: Add внутри горутины, пропуск Done при панике.
  • Предложение sync.Map как универсальной замены map - это ошибка, она эффективна только в узких сценариях.
  • Игнорирование sync/atomic - для простых счётчиков мьютекс избыточен.
  • Упоминание Cond без понимания, зачем нужен цикл for вокруг Wait - ложные пробуждения.
  • Смешивание каналов и мьютексов в одном решении без явной необходимости - признак плохого проектирования.

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

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