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.Isvserrors.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-секция награждает за понимание, а не за заучивание. Разберитесь, как работает рантайм, потренируйтесь объяснять это вслух — и вопросы про планировщик и каналы перестанут пугать.