Подготовка к собеседованию по Go

Вопросы на собеседовании Go-разработчика

Подборка вопросов по Go для backend-разработчиков, сгруппированная по темам и связанная с тем же каталогом, который используется в практике EngineerSpeak.

Начать ИИ-собеседование по GoБанковская карта не нужна. Доступна 1 бесплатная сессия.
Практика технического интервью на английскомРежим, в котором не носители языка могут потренироваться проходить интервью.

Система типов

1Объясните, как в Go работают нулевые значения для встроенных и ссылкоподобных типов и почему они важны при объявлении переменных без явной инициализации.

В Go переменная, объявленная без явного инициализатора, автоматически инициализируется нулевым значением своего типа. Числовые типы получают 0, bool — false, string — "", а массивы и struct обнуляются поэлементно или по полям. Для pointer-подобных или ссылкоподобных типов, таких как pointers, slices, maps, channels, functions и interfaces, нулевым значением является nil. Это важно, потому что переменные Go и пропущенные поля struct начинают жизнь в детерминированном состоянии, а не содержат мусорные значения, и многие API спроектированы так, что нулевое значение является полезным значением по умолчанию, хотя некоторые nil-значения всё же требуют инициализации перед определёнными операциями.

Попробовать ответить на вопрос AI-тренеру

2Как Go обрабатывает равенство структур и что происходит, если структура содержит несравнимые поля?

Значения структур в Go можно сравнивать с помощью `==` и `!=` только когда каждое поле структуры comparable. Равенство сравнивает соответствующие поля по правилам равенства каждого поля. Если структура содержит неcomparable-поле, например срез, map или функцию, тип структуры не comparable, и сравнение двух значений такого типа через `==` — ошибка компиляции. Для таких структур используйте пользовательскую логику сравнения или подходящий помощник глубокого равенства, особенно в тестах.

Попробовать ответить на вопрос AI-тренеру

3Объясните неизменяемость строк в Go и связь между string, []byte, bytes.Buffer и strings.Builder.

В Go `string` — это неизменяемая последовательность байтов, часто UTF-8 текст, но не обязательно валидный UTF-8. Строку нельзя изменить на месте; чтобы изменить содержимое, обычно конвертируют в `[]byte` для правок на уровне байтов или в `[]rune` для правок на уровне code point, а затем конвертируют обратно. Обычные преобразования между `string` и `[]byte` копируют данные и могут выделять память, поэтому повторные преобразования или повторная конкатенация в циклах могут быть дорогими. `strings.Builder` оптимизирован для эффективной сборки строк, а `bytes.Buffer` — изменяемый байтовый буфер, полезный для байтовых данных и I/O, и тоже может дать строку.

Попробовать ответить на вопрос AI-тренеру

4Объясните, чем поведение nil отличается для указателей, срезов, map, каналов, функций и интерфейсов в Go.

В Go nil — это нулевое значение для указателей, срезов, map, каналов, функций и интерфейсов, но операции с такими nil-значениями зависят от типа. Nil-указатель можно сравнить с nil, но разыменование вызывает panic. У nil-среза length и capacity равны 0; по нему можно итерироваться и к нему можно делать append. Из nil-map можно читать и по ней можно range, но запись в неё вызывает panic. Отправка в nil-канал или получение из него блокируется навсегда, а закрытие nil-канала вызывает panic. Вызов nil-функции вызывает panic. Интерфейс равен nil только когда у него нет ни динамического типа, ни динамического значения; интерфейс, содержащий типизированное nil-значение (например, nil-указатель), сам по себе не равен nil.

Попробовать ответить на вопрос AI-тренеру

5Что такое comparable-типы в Go и как правила сравнимости влияют на ключи map, равенство и ограничения в дженериках?

Comparable-типы в Go — это типы, значения которых можно сравнивать с помощью `==` и `!=`. Базовые типы, указатели, каналы, интерфейсы, а также структуры/массивы, у которых все поля или элементы comparable, являются comparable; срезы, map и функции не comparable, за исключением сравнения с `nil`. Ключи map должны быть comparable. Равенство следует правилам сравнения типа, а сравнение интерфейсов зависит от динамических конкретных значений; если сравниваемый интерфейс содержит неcomparable динамическое значение, сравнение вызывает panic. В дженериках предопределённое ограничение `comparable` позволяет параметрам типа сравниваться через `==`/`!=` и использоваться как ключи map.

Попробовать ответить на вопрос AI-тренеру

6Как Go представляет байты, руны и UTF-8 текст и почему len(s) может отличаться от числа видимых пользователю символов?

В Go `byte` — это псевдоним `uint8` и обозначает один «сырой» байт, а `rune` — псевдоним `int32` и обозначает Unicode code point. `string` — это read-only последовательность байтов, обычно UTF-8 текст, но может содержать произвольные байты. `len(s)` возвращает число байтов, а не рун или видимых пользователю символов. Индексация строки возвращает байт; range по строке декодирует UTF-8 и даёт байтовые индексы плюс руны. `len(s)` может отличаться от числа видимых символов, потому что UTF-8 может использовать несколько байтов на code point и потому что один видимый пользователю символ может состоять из нескольких code point, например combining marks или emoji-последовательности.

Попробовать ответить на вопрос AI-тренеру

Структуры данных

7Опишите разницу между массивами и срезами в Go, включая поведение length, capacity и нижележащего хранилища.

Массив в Go имеет фиксированную длину, которая является частью его типа, например [3]int; он хранит элементы непосредственно, а присваивание или передача массива копирует всё значение массива. Срез, например []int, — это небольшой дескриптор над нижележащим массивом: концептуально он содержит указатель на элементы, length и capacity. Length среза — число видимых элементов; capacity — сколько элементов можно использовать от начала среза до конца backing-массива. Срезы гибкие: reslicing меняет дескриптор, а append может повторно использовать тот же нижележащий массив, если позволяет capacity, или выделить новый, если нет.

Попробовать ответить на вопрос AI-тренеру

8Как ведёт себя тип map в Go в отношении типов ключей, отсутствующих ключей, nil-map и порядка итерации?

Типы ключей map в Go должны быть comparable; срезы, map и функции нельзя напрямую использовать как ключи. Поиск ключа, которого нет, возвращает нулевое значение типа элемента, поэтому форму comma-ok (`v, ok := m[k]`) используют, чтобы отличить отсутствие от присутствующего нулевого значения. Из nil-map можно читать и по ней можно range, но запись в неё вызывает panic; перед записью её нужно инициализировать. Порядок итерации по map не специфицирован, и код не должен от него зависеть.

Попробовать ответить на вопрос AI-тренеру

9Опишите, как reslicing и присваивание срезов могут привести к тому, что несколько срезов разделяют один и тот же нижележащий массив, и какие баги это может создать.

Значение среза — это заголовок, указывающий внутрь нижележащего массива. Присваивание среза или передача его в функцию копирует только этот заголовок, а не элементы. Reslicing создаёт другой заголовок, указывающий на диапазон того же backing-массива. Поэтому несколько срезов могут быть алиасами одного хранилища: изменение элемента через один срез может быть видно через другой, а append к одному срезу может перезаписать данные, видимые другому, если ещё есть запас capacity. Баги включают неожиданные мутации, испорченные результаты, удержание больших backing-массивов через маленькие подсрезы и data race при конкурентном использовании алиасов. Чтобы избежать непреднамеренного разделения, сделайте защитную копию через copy или append([]T(nil), s...), либо ограничьте capacity полным выражением среза перед append.

Попробовать ответить на вопрос AI-тренеру

10Объясните рост среза при append на концептуальном уровне и влияние на производительность повторных перевыделений.

Когда append добавляет элементы в срез, он записывает их в существующий backing-массив, если у среза достаточно capacity. Если capacity недостаточна, Go выделяет больший backing-массив, копирует существующие элементы, записывает новые и возвращает заголовок среза, указывающий на новое хранилище. Точная политика роста зависит от реализации, но концептуально capacity растёт достаточно, чтобы повторные append были амортизированно эффективны. Повторные перевыделения всё равно стоят CPU на копирование, создают аллокации, увеличивают давление на GC и могут разорвать разделение со старыми алиасами срезов. Если ожидаемый размер известен, предварительно выделяйте с make([]T, 0, n) при построении через append или make([]T, n) при заполнении по индексу, чтобы уменьшить перевыделения.

Попробовать ответить на вопрос AI-тренеру

Семантика языка

11Как в Go работает blank identifier для неиспользуемых значений, импортов и compile-time проверок интерфейсов?

Blank identifier `_` — это write-only placeholder. Присваивание ему отбрасывает значение и не создаёт usable-переменную. Его используют, чтобы игнорировать ненужные возвращаемые значения или переменные цикла, чтобы импортировать пакет только ради side effects через `import _ "pkg"`, и чтобы делать compile-time проверки реализации интерфейса вроде `var _ io.Reader = (*MyReader)(nil)`. Blank import всё равно запускает инициализацию импортированного пакета. Присваивание для проверки интерфейса не компилируется, если method set конкретного типа не удовлетворяет интерфейсу.

Попробовать ответить на вопрос AI-тренеру

Пакеты

12Как в Go работает порядок инициализации пакетов, включая функции init и импортированные зависимости?

Go инициализирует пакеты в порядке зависимостей. Импортированные зависимости пакета инициализируются до импортирующего пакета. Внутри пакета package-level переменные инициализируются до любых функций `init`, причём инициализация переменных упорядочена по зависимостям и порядку объявления, как определено языком. Затем автоматически выполняются функции `init` пакета; в пакете может быть несколько `init`, и их нельзя вызывать напрямую. Каждый пакет инициализируется один раз. Для исполняемого файла сначала инициализируется граф импортов, затем инициализируется пакет `main`, и наконец вызывается `main.main`.

Попробовать ответить на вопрос AI-тренеру

13Объясните правила видимости пакетов в Go, включая экспортируемые идентификаторы и соглашение о каталоге internal/.

В Go видимость пакета управляется именованием идентификаторов, а не ключевыми словами доступа. Идентификатор, имя которого начинается с заглавной Unicode-буквы, экспортируется и может быть использован из других пакетов; остальные идентификаторы неэкспортируемы и доступны только внутри того же пакета. Это относится к функциям, типам, методам, переменным, константам и полям структур. Пакеты используют экспортируемые идентификаторы, чтобы определить свой public API, и оставляют детали реализации неэкспортируемыми. Отдельно пакет, расположенный под каталогом `internal/`, может импортироваться только кодом, чей import path находится внутри родительского дерева этого каталога `internal`; это enforced toolchain'ом Go.

Попробовать ответить на вопрос AI-тренеру

Управление потоком

14Как работает defer в Go, включая порядок выполнения, момент вычисления аргументов и взаимодействие с возвращаемыми значениями?

`defer` планирует вызов функции на момент выхода из окружающей функции — как при обычном return, так и при раскрутке panic. Несколько отложенных вызовов выполняются в порядке last-in, first-out. Значение отложенной функции и её аргументы вычисляются сразу при выполнении оператора `defer`, а сам вызов происходит позже. При именованных возвращаемых значениях оператор return сначала присваивает возвращаемые значения, затем выполняются отложенные функции, поэтому отложенное замыкание может наблюдать или изменять именованные result-переменные до того, как их получит вызывающий код. Поэтому `defer` удобен для cleanup: закрытия файлов, Unlock мьютексов и освобождения ресурсов.

Попробовать ответить на вопрос AI-тренеру

Обработка ошибок

15Объясните модель обработки ошибок в Go и общепринятые способы создания, возврата и проверки ошибок.

Go рассматривает ошибки как обычные значения, а не как исключения. Встроенный интерфейс `error` удовлетворяется любым типом с методом `Error() string`. По соглашению функции возвращают `error` последним результатом: `nil` означает успех, а ненулевая ошибка — что вызывающий должен обработать или пробросить сбой. Простые ошибки обычно создают через `errors.New`, форматированные — через `fmt.Errorf`, а вызывающий обычно проверяет `if err != nil { ... }`.

Попробовать ответить на вопрос AI-тренеру

16How should panic recovery be handled in Go backend services, including what happens when a goroutine panics and when a process should recover versus crash?

A panic unwinds the current goroutine, running its deferred functions. `recover` only works when called from a deferred function in that same goroutine; one goroutine cannot recover another goroutine's panic. If a panic is not recovered, the process crashes. In backend services, recovery should usually be placed at isolation boundaries such as request handlers, RPC middleware, or worker goroutine entrypoints so one failing request or job does not take down the whole service. But if a panic may have corrupted shared state or made process integrity untrustworthy, it is safer to let the process crash and restart rather than recover and continue blindly.

Попробовать ответить на вопрос AI-тренеру

Конкурентность

17Что такое nil channels в Go и как они могут случайно ломать код или намеренно отключать select cases?

Nil channel — это переменная channel со значением nil, часто потому что её не инициализировали через make или явно присвоили nil. Send в nil channel или receive из него блокируются навсегда. В select case с nil channel никогда не готов, поэтому присвоение переменной channel значения nil может намеренно отключить этот case. Случайное использование nil channel может завесить goroutines или заставить select-логику перестать обрабатывать ожидаемые события.

Попробовать ответить на вопрос AI-тренеру

18Чем атомарные операции в sync/atomic отличаются от синхронизации на mutex и когда они уместны?

sync/atomic предоставляет неделимые операции над отдельными ячейками памяти, такие как load, store, add, swap и compare-and-swap, с гарантиями синхронизации/memory-ordering. Mutex защищает критическую секцию, поэтому может охранять произвольный код и инварианты, затрагивающие несколько чтений, записей или полей. Atomics уместны для простого независимого состояния вроде счётчиков, флагов, sequence numbers или тщательно спроектированных lock-free структур. Предпочитайте mutex, когда операции составные, несколько значений должны оставаться согласованными, или атомарная версия была бы трудна для рассуждений и доказательства корректности.

Попробовать ответить на вопрос AI-тренеру

19Как следует проектировать владение channel и жизненный цикл горутин, чтобы избегать goroutine leaks?

Проектируйте горутины с явным владельцем, ясным сигналом shutdown и гарантированным путём выхода. Сторона-producer обычно владеет закрытием channel, особенно output channel; receivers не должны закрывать channel, пока senders ещё могут быть активны. Каждая блокирующая send, receive, loop, timer или внешний вызов должны либо гарантированно завершаться, либо уметь разблокироваться по cancellation, обычно через context.Context или done channel. Используйте WaitGroup, errgroup или похожую координацию, чтобы дожидаться workers и закрывать channels только после выхода senders.

Попробовать ответить на вопрос AI-тренеру

20What are common causes of goroutine leaks in Go services, and how do you detect and fix them in production?

Common goroutine leaks in Go services come from goroutines blocked forever on channel sends or receives, waiting on other blocking operations without cancellation, stuck I/O without deadlines, background loops or tickers that never stop, and request-scoped goroutines that outlive the request. In production, you look for sustained growth in goroutine count and related symptoms, then inspect goroutine dumps or pprof goroutine profiles to see where goroutines are stuck. Fixing the leak means changing the code so those goroutines can exit: add cancellation and deadlines, stop tickers, close channels correctly, avoid detached request goroutines, and bound concurrency where needed.

Попробовать ответить на вопрос AI-тренеру