Go-секция на собеседовании устроена предсказуемо: круг тем конечен. Но поверхностные ответы здесь видно сразу. «Горутина — это лёгкий поток» проходит на джуниоре, но на middle/senior за этой фразой обязательно последует «а что значит лёгкий?» и «кто их планирует?». Ниже — как устроена секция и реальные вопросы по блокам с ответами-ориентирами: не заучивайте формулировки, а поймите, что именно проверяет каждый вопрос.

Как устроена Go-секция на интервью

Типичная секция длится 45–90 минут и состоит из трёх частей. Теория — быстрые вопросы на понимание рантайма и языка: проверяют, что вы понимаете поведение кода, а не просто его пишете. Лайвкодинг — чаще конкурентная задача (worker pool, ограничение параллелизма, graceful shutdown) или работа со структурами данных; алгоритмы встречаются реже. Вопросы по опыту — «расскажите про самый сложный баг с гонкой», «как профилировали сервис». Глубина зависит от грейда: на middle копают в «знаешь и умеешь», на senior — в «понимаешь trade-offs и видел грабли».

Горутины и планировщик (GMP)

  • Чем горутина отличается от OS-потока? Горутина дешевле: начальный стек ~2 КБ против мегабайтов у потока, стек растёт и сжимается динамически. Планирует их рантайм Go в user space, без перехода в ядро на каждое переключение.
  • Что такое модель GMP? G — горутина, M — OS-поток (machine), P — логический процессор (контекст планирования). Горутины (G) выполняются на потоках (M), но только держа P. Число P задаётся GOMAXPROCS (по умолчанию — число доступных CPU).
  • Что такое work stealing? У каждого P своя локальная очередь горутин. Когда очередь P пустеет, он «крадёт» половину горутин у другого P или берёт из глобальной очереди — так балансируется нагрузка.
  • Как работает вытеснение горутин? До Go 1.14 планировщик был кооперативным (переключение в точках вызова функций), и горутина с тугим циклом без вызовов могла залочить P. С Go 1.14 добавлена асинхронная preemption через сигналы — рантайм прерывает горутину почти в любой точке. При блокирующем syscall M вместе с G отвязывается, а P отдаётся другому M.

Каналы

  • Buffered vs unbuffered? Unbuffered (make(chan T)) синхронный: отправка блокируется, пока кто-то не читает (рандеву). Buffered (make(chan T, n)) принимает до n значений без блокировки, дальше блокируется.
  • Что делает select? Ждёт готовности одного из каналов; если готовы несколько — выбирает случайный. default делает операцию неблокирующей.
  • Когда возникает deadlock? Когда все горутины заблокированы, например запись в unbuffered-канал без читателя в той же горутине. Рантайм паникует с all goroutines are asleep - deadlock!.
  • Чтение из закрытого канала? Возвращает оставшиеся значения, потом нулевое значение типа; v, ok := <-ch даёт ok == false. Запись в закрытый канал или повторное закрытие — паника.
  • Чтение/запись в nil-канал? Блокируется навсегда. Иногда используют осознанно: занулить кейс в select, чтобы его отключить.

Слайсы и мапы изнутри

  • Из чего состоит слайс? Из трёх полей: указатель на массив, длина (len) и ёмкость (cap). Слайс — это «окно» над базовым массивом.
  • Как растёт слайс при append? Если cap исчерпан, выделяется новый массив и данные копируются. Множитель роста амортизированный, зависит от размера и менялся между версиями (для больших слайсов ближе к 1,25×) — не закладывайтесь на конкретное число.
  • Почему изменения внутри функции «теряются»? Слайс передаётся по значению (копируется заголовок). Изменения элементов видны — указатель тот же, — но append с реаллокацией создаёт новый массив, и снаружи изменений нет.
  • Что такое эвакуация в мапе? При росте мапа аллоцирует новые бакеты и инкрементально переносит туда старые пары. Поэтому порядок итерации не гарантирован и рандомизирован.
  • Можно ли брать адрес элемента мапы? Нет, &m[k] не компилируется: значения мапы не адресуемы, потому что могут переехать при росте.

Интерфейсы

  • Чем iface отличается от eface? eface — пустой интерфейс (any), хранит указатели на тип и на данные. iface — интерфейс с методами, хранит itab (тип + таблица методов) и указатель на данные.
  • Главная гоча с nil-интерфейсом? Интерфейс равен nil, только если оба указателя (на тип и на данные) пустые. Если вернуть (*MyErr)(nil) как error, тип есть — и err != nil даст true. Классический баг.
  • Как проверить, реализует ли тип интерфейс? Через type assertion v, ok := x.(io.Reader) или compile-time проверку var _ io.Reader = (*MyType)(nil).
  • Когда интерфейс, а когда дженерики (Go 1.18+)? Интерфейс — когда важно поведение (методы). Дженерики — когда нужна одна логика для разных типов с типобезопасностью без any и рефлексии.

Context

  • Зачем нужен context? Для передачи сигнала отмены, дедлайнов и request-scoped значений через цепочку вызовов. Главное применение — отмена долгих операций и таймауты.
  • Как отменить операцию? ctx, cancel := context.WithCancel(parent); вызов cancel() закрывает ctx.Done(), и слушатели завершаются. cancel вызывают всегда (часто через defer), иначе утечка.
  • WithTimeout vs WithDeadline? WithTimeout — относительный срок («через 2 с»), WithDeadline — абсолютный момент. Под капотом первый — обёртка над вторым.
  • Что хранить в context.Value? Только request-scoped данные (trace id, auth), не обязательные параметры функции. Ключи — приватные типы против коллизий.

Ошибки

  • errors.Is vs errors.As? Is сравнивает с конкретной ошибкой-целью по цепочке обёрток (errors.Is(err, sql.ErrNoRows)). As извлекает ошибку нужного типа в переменную (var pe *PathError; errors.As(err, &pe)).
  • Как обернуть ошибку? fmt.Errorf("...: %w", err) — глагол %w сохраняет оригинал в цепочке, и Is/As до него докопаются. %v — теряет цепочку.
  • Что такое errors.Join (Go 1.20)? Объединяет несколько ошибок в одну; Is/As проверяют каждую. Удобно при сборе ошибок из нескольких операций.
  • panic vs error? Ошибка — ожидаемая ситуация, обрабатывается явно. panic — для невосстановимых багов. recover ловят на границах (HTTP middleware), но не как замену исключениям.

GC и память

  • Что такое escape analysis? Анализ компилятора, решающий, где разместить переменную — на стеке (дёшево, освобождается с фреймом) или в куче (требует GC). Если ссылка «убегает» за пределы функции, значение уходит в кучу.
  • Как посмотреть, что ушло в кучу? go build -gcflags='-m' печатает решения escape analysis. Полезно при оптимизации горячих путей.
  • Как работает GC в Go? Конкурентный, трёхцветный mark-and-sweep с короткими stop-the-world паузами. Цель — низкая латентность, а не максимальный throughput.
  • Что такое GOGC и GOMEMLIMIT? GOGC (по умолчанию 100) задаёт, насколько вырастет куча до следующей сборки. GOMEMLIMIT (Go 1.19) — мягкий лимит памяти против OOM.
  • Зачем sync.Pool? Переиспользовать аллокации (буферы) и снять нагрузку с GC. Объекты могут быть собраны в любой момент — состояние там не хранят.

Конкурентные паттерны

  • Когда sync.Mutex, а когда каналы? Мьютекс — для защиты разделяемого состояния (счётчик, кэш). Каналы — для передачи владения данными и координации. Девиз: «share memory by communicating», но мьютекс для простого состояния часто проще и быстрее.
  • Зачем errgroup? Запускает группу горутин, собирает первую ошибку и отменяет общий context. WithContext плюс SetLimit дают worker pool с ограничением параллелизма.
  • Как сделать worker pool? Пул горутин читает задачи из канала; число воркеров ограничивает параллелизм. Альтернатива — семафор через буферизированный канал или errgroup.SetLimit.
  • Как ловить гонки данных? Флаг -race (go test -race) включает детектор гонок в рантайме. Ждут, что вы про него знаете и гоняете тесты с ним в CI.
  • Что такое утечка горутины? Горутина, заблокированная навсегда (например пишет в канал, который никто не читает). Не собирается GC, накапливается. Лечится context и закрытием каналов.

Чего ждут от middle, а чего от senior

Темы те же — отличаются глубина и угол. На middle достаточно уверенно применять язык: знать buffered/unbuffered, не словить nil interface, корректно отменять через context, написать рабочий worker pool. От планировщика хватит модели GMP и понимания, что горутины дешёвые.

На senior те же вопросы превращаются в разговор о trade-offs. Не «как сделать worker pool», а «почему здесь каналы, а не семафор, и как это ляжет на GC под нагрузкой». Ждут историй из прода: дебаг гонки с -race, утечка горутин через pprof, тюнинг GOGC/GOMEMLIMIT. Senior должен обосновать каждое решение и признать, где компромисс.

Как готовиться

Сначала прогоните темы списком: по каждому блоку выше проговорите ответ вслух, как интервьюеру. Если на «почему» спотыкаетесь — копайте глубже, читайте исходники рантайма и блог Go. Затем закрепите практикой: worker pool с graceful shutdown, гонка под -race, escape analysis на своём коде.

Хорошо ускоряет подготовку тренировка с LLM — можно гонять mock-интервью по Go ежедневно и просить жёсткую критику ответов. Подробно про рабочие промпты и ограничения — в гайде «Подготовка к собеседованию с LLM». Но LLM не заменит давления живого диалога, поэтому финальный шаг — мок-собеседование с senior-инженером, который копает в ваши ответы как настоящий интервьюер.

В сообществе IT-ХОЗЯЕВА мок-собеседования и доступ к базе менторов (практикующие инженеры, включая разработчиков с продакшен-опытом Go) входят в тариф ХОЗЯИН за 1000 ₽/мес — там же разбирают резюме и готовят под конкретную позицию. Удобно, если нужна не разовая сессия, а регулярная практика с фидбеком.

Go-секция награждает за понимание, а не за заучивание. Разберитесь, как работает рантайм, потренируйтесь объяснять это вслух — и вопросы про планировщик и каналы перестанут пугать.