Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Как устроен sync.Once и почему копия Once — баг?
sync.Once держит атомарный флаг и мьютекс: первый Do выполняет функцию под мьютексом и ставит флаг атомарно; остальные Do видят флаг и выходят. Гарантия сильная: функция выполнится РОВНО один раз, даже при панике внутри (начиная с Go 1.21 проваленная паникой инициализация считается выполненной — позже обсудили и смягчили, знать о споре стоит). Копирование Once — классическая ошибка: копия получает снимок флага, и оба «Once» выполнят функцию независимо. Поэтому Once живут по указателю или внутри структуры, которая не копируется.
type Client struct {
once sync.Once
conn *Conn
}
func (c *Client) get() *Conn {
c.once.Do(func() { c.conn = connect() }) // корректно: через указатель
return c.conn
}2. Чем RWMutex отличается от Mutex и когда RLock НЕ помогает?
RWMutex допускает много параллельных читателей и одного писателя: RLock берётся многими, Lock — эксклюзивно. Выигрыш реален только при read-heavy нагрузке и достаточно долгих критических секциях: на коротких операциях накладные расходы RWMutex (атомарные счётчики читателей) съедают выгоду, и обычный Mutex быстрее. Ловушки: (1) RLock не апгрейдится до Lock — попытка взять Lock под RLock = дедлок; (2) writer starvation в старых версиях — современные реализации ставят ожидающего писателя барьером для новых читателей. Практика: начинать с Mutex, переходить на RWMutex по профилю с реальной read-heavy нагрузкой.
3. Когда sync.Map действительно быстрее? Почему не «всегда»?
sync.Map оптимизирован под два сценария: (1) ключи записываются один раз, читаются много раз (кэши конфигурации, реестры); (2) горутины работают с непересекающимися наборами ключей. Устройство: чтение идёт по атомарному указателю на read-слот без блокировки, записи перестраивают read/dirty. Цена: записи дороже, len() O(n), range по снимку. Для обычных нагрузок «пишем и читаем вперемешку» мьютекс + обычная карта быстрее и предсказуемее. Ответ-формула: sync.Map — не замена map+Mutex, а специализированный кейс, подтверждённый профилем.
4. Что делает sync.Cond и почему его почти не используют?
Cond — условная переменная: горутины ждут сигнала (Wait) о «условие могло измениться» и просыпаются от Broadcast/Signal. Требует мьютекс-пару и цикл проверки условия (Wait может проснуться ложно — спууриос-пробуждение; поэтому всегда for !condition { c.Wait() }). Почти не используется, потому что те же задачи чище решаются каналами (закрытие канала будит всех — как Broadcast) и контекстом. Знать Cond стоит для чтения чужого кода (стандартная библиотека использует) и для вопросов «как бы вы реализовали блокирующую очередь без каналов».
c := sync.NewCond(&mu)
// ждущий:
mu.Lock()
for queue.IsEmpty() { c.Wait() }
job := queue.Pop()
mu.Unlock()
// добавляющий:
mu.Lock(); queue.Push(job); c.Broadcast(); mu.Unlock()5. Что такое Weighted (golang.org/x/sync/semaphore) и чем лучше канального семафора?
Weighted — семафор с весами: Acquire(ctx, n) занимает n единиц, Release(n) возвращает; полезен, когда ресурсы неоднородны — например, скачивание файлов весом в размере (мелкие файлы не занимают «слот» целиком). Канальный семафор из темы про паттерны даёт только единичные слоты. Плюс Weighted: Acquire принимает контекст — ожидание слота отменяется вместе с запросом; канальному варианту для этого нужен select. В ответе стоит назвать и лимит: чем больше весов в системе, тем вероятнее нехватка из-за дробления — веса стоит подбирать с запасом.
sem := semaphore.NewWeighted(100) // например, 100 МБ лимита err := sem.Acquire(ctx, sizeOf(file)) // вес = размер файла defer sem.Release(sizeOf(file))
6. Mutex — справедливый? Может ли горутина вечно ждать Lock?
Mutex Go не строго FIFO: рантайм балансирует пропускную способность и справедливость — новый захват в момент освобождения может «проскочить» мимо очереди ждущих (если мьютекс в незаблокированном «разогретом» состоянии), но голодание писателей/читателей бесконечно не допускается: после порога ожидания сквозняки прекращаются, и очередь честно раздаётся. Аналогичная история с RWMutex: писатель блокирует новых читателей. Практический вывод: экстремальная конкуренция за один мьютекс — сигнал дробить состояние или пересматривать дизайн, а не искать «справедливый» примитив.
7. Как проверить, что примитив выбран правильно? На что смотреть в профиле?
Инструменты: (1) pprof блокировок (block profile) и мьютексных ожиданий (mutex profile) с runtime.SetBlockProfileRate/SetMutexProfileFraction — показывают, где горутины ждут и на каком мьютексе; (2) -race — корректность; (3) бенчмарки с -benchmem и p99-метрики под нагрузкой — итоговое сравнение вариантов. Признаки неверного примитива: конкуренция на мьютексе видна в mutex profile (виновник — конкретный Lock), длина очереди горутин растёт, латентность «пилит» синхронно с GC или с ростом RPS. Ответ уровня senior: «выбор подтверждается профилем и метриками латентности, а не бенчмарком в вакууме».
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Asynq: полный курс по фоновым задачам в Go — Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.