Вопросы на собеседовании: Go-разработчик
100 реальных вопросов с образцовыми ответами и пояснениями для уровня Junior Go-разработчик.
Смотреть пример резюме: Go-разработчик →Тренировка флешкарточками
Интервальное повторение · Hunter Pass
Вопросы
Предопределённые базовые типы Go делятся на логические, числовые и строковые.
- bool представляет значения true и false.
- К числовым типам относятся знаковые и беззнаковые целые числа, числа с плавающей точкой и комплексные числа.
- string представляет неизменяемую последовательность байтов.
Зачем это спрашивают: Эти категории описывают фундаментальные несоставные значения, доступные без определения пользовательских типов.
Обе формы объявляют переменные, но различаются правилами области видимости и указания типа.
- var работает на уровне пакета или функции и может включать явный тип, начальное значение либо и то и другое.
- Объявление var без явного типа выводит его из начального значения, а без начального значения использует zero value этого типа.
- := работает только внутри функций и всегда выводит типы объявляемых переменных.
- Короткое объявление нескольких переменных может повторно использовать переменные из того же блока, только если хотя бы одна переменная, кроме _, является новой.
Зачем это спрашивают: Короткая форма удобна для локального вывода типов, а var поддерживает любую область объявления и явное указание типа.
Нулевое значение присваивается переменной по умолчанию, если она создана без явного начального значения.
- Логические значения по умолчанию равны false, числовые равны 0, а строки являются пустыми.
- Указатели, срезы, карты, функции, каналы и интерфейсы по умолчанию равны nil.
- Каждый элемент массива изначально содержит нулевое значение типа элемента.
- Каждое поле структуры изначально содержит нулевое значение типа поля.
Зачем это спрашивают: Нулевые значения делают каждую объявленную переменную допустимым значением своего типа ещё до явного присваивания.
Константы Go могут быть нетипизированными или явно типизированными, а iota помогает создавать связанные наборы констант.
- Нетипизированная константа сохраняет абстрактный вид константы, пока контекст не потребует конкретный тип.
- Типизированная константа имеет явный тип из объявления, и её значение должно быть представимо этим типом.
- Значения констант фиксируются во время компиляции и не могут изменяться после объявления.
- Внутри блока const значение iota начинается с 0 и увеличивается на 1 для каждого следующего объявления константы.
Зачем это спрашивают: Эта модель сохраняет точность констант и позволяет кратко объявлять типизированные значения и перечисления.
Явное преобразование в Go применяет целевой тип в форме T(value).
- Go не выполняет неявные преобразования между разными числовыми типами, например int32 и int64.
- Исходное значение должно допускать преобразование в целевой тип по правилам языка.
- При преобразовании числа с плавающей точкой в целое дробная часть отбрасывается.
- Сужение диапазона или смена числового представления может привести к потере диапазона или точности.
Зачем это спрашивают: Явные преобразования делают смену представления заметной и заставляют учитывать возможную потерю данных.
Строка Go хранит доступную только для чтения последовательность байтов, содержимое которой нельзя изменить на месте.
- Строка может содержать произвольные байты и не обязана быть корректной последовательностью UTF-8.
- Индексация строки возвращает байт в указанной позиции.
- Срез строки выбирает диапазон байтов и создаёт другое строковое значение.
- Для изменения текста нужно создать новую строку, часто через преобразование в []byte или []rune.
Зачем это спрашивают: Неизменяемость запрещает прямое обновление байтов, а байтовое представление сохраняет простоту хранения строк.
byte и rune являются предопределёнными псевдонимами для двух разных представлений текстовых или бинарных данных.
- byte является псевдонимом uint8 и представляет один необработанный байт.
- rune является псевдонимом int32 и по соглашению представляет одну кодовую точку Unicode.
- Символьный литерал в одинарных кавычках создаёт значение rune.
- Индексация строки работает с байтами, а range по строке декодирует кодовые точки UTF-8 в значения rune.
Зачем это спрашивают: Выбор byte или rune показывает, работает ли код с необработанными закодированными данными или с кодовыми точками Unicode.
Для строки len сообщает количество байтов, а не декодированных кодовых точек Unicode.
- Символы ASCII занимают один байт, поэтому для строк только из ASCII количество байтов и rune совпадает.
- utf8.RuneCountInString считает значения rune, полученные при декодировании UTF-8.
- Преобразование в []rune с последующим вызовом len тоже считает декодированные rune, но создаёт срез rune.
- Количество rune не всегда равно числу символов, воспринимаемых пользователем, потому что один видимый знак может состоять из нескольких кодовых точек.
Зачем это спрашивают: Разделение длины в байтах и количества rune помогает избежать неверных предположений о тексте UTF-8 переменной ширины.
Функция Go может объявлять и возвращать несколько значений в фиксированном порядке.
- Сигнатура функции перечисляет все типы результатов, а каждый return передаёт соответствующие значения.
- Результаты вызова с несколькими значениями можно присвоить такому же количеству переменных.
- Blank identifier _ отбрасывает значение, не создавая доступную для использования привязку.
- Любая другая локальная переменная должна использоваться, поэтому _ позволяет явно игнорировать ненужный результат.
Зачем это спрашивают: Несколько результатов напрямую выражают связанные выходные значения, а blank identifier позволяет намеренно пропустить одно из них.
Объявление именованного типа создаёт отдельный тип, а псевдоним даёт другое имя существующему типу.
- type UserID int определяет UserID как новый именованный тип с базовым типом int.
- При передаче значений между UserID и int обычно требуется явное преобразование.
- type UserID = int делает UserID идентичным int с точки зрения идентичности типов и присваивания.
- Псевдоним не создаёт отдельный тип или отдельное базовое представление.
Зачем это спрашивают: Именованные типы получают отдельную идентичность, а псевдонимы сохраняют идентичность целевого типа.
Массив имеет фиксированную длину, которая входит в его тип, а срез является гибким представлением нижележащего массива.
- Длина входит в тип массива, поэтому [3]int и [4]int являются разными типами.
- При присваивании массива копируются все его элементы.
- Срез может менять длину в пределах своей емкости и расти с помощью append.
- При присваивании среза копируется его дескриптор, поэтому оба среза могут ссылаться на одни элементы.
Зачем это спрашивают: Массивы хранят элементы непосредственно, а срезы описывают доступ к элементам нижележащего массива.
Срез описывается указателем на нижележащий массив, длиной и емкостью.
- Указатель определяет, где начинаются элементы среза.
- Длина показывает количество элементов, доступных через срез в данный момент.
- Емкость показывает количество доступных элементов от начала среза до конца нижележащего хранилища.
- При копировании значения среза копируется этот дескриптор, а не нижележащие элементы.
Зачем это спрашивают: Дескриптор из трех частей позволяет Go создавать легковесное представление данных массива.
Nil-срез не ссылается на нижележащее хранилище, а пустой срез, не равный nil, инициализирован, но не содержит элементов.
- У обоих длина и емкость могут быть равны нулю.
- Только nil-срез равен nil при сравнении.
- append, len, cap и range работают с обоими видами срезов.
- make([]T, 0) и []T{} создают пустые срезы, не равные nil.
Зачем это спрашивают: Различие важно, когда код или сериализация должны отличать nil от инициализированного пустого значения.
Функция make создает инициализированный срез с указанной длиной и необязательной емкостью.
- make([]T, length) создает срез, емкость которого не меньше его длины.
- make([]T, length, capacity) задает оба значения, причем емкость не может быть меньше длины.
- Все элементы в пределах начальной длины содержат нулевое значение типа T.
- make возвращает значение среза, а не указатель на срез.
Зачем это спрашивают: Использование make подготавливает нижележащее хранилище и дескриптор среза за одну операцию.
append возвращает срез, содержащий исходные элементы, за которыми следуют новые.
- Если емкости достаточно, append может повторно использовать существующий нижележащий массив.
- Если емкости недостаточно, append выделяет новое нижележащее хранилище и копирует элементы.
- Возвращенный срез нужно использовать, потому что его указатель, длина или емкость могут измениться.
- При повторном использовании хранилища другие срезы с тем же массивом могут увидеть изменения элементов.
Зачем это спрашивают: Из-за возможного перевыделения результат append обычно присваивают обратно срезу.
Подсрез обычно использует тот же нижележащий массив, что и исходный срез.
- Изменение общего элемента через один срез видно через другой.
- Длина подсреза определяется выбранным диапазоном индексов.
- Его емкость обычно продолжается от новой начальной позиции до предела емкости исходного среза.
- Полное срезающее выражение позволяет задать подсрезу меньший предел емкости.
Зачем это спрашивают: Общее нижележащее хранилище делает создание срезов дешевым, но позволяет разным ссылкам изменять одни элементы.
copy переносит элементы из исходного среза в существующий целевой срез.
- Она копирует min(len(dst), len(src)) элементов.
- Она возвращает количество скопированных элементов.
- Она не увеличивает целевой срез и не меняет его длину.
- Она дает правильный результат, даже если исходный и целевой диапазоны перекрываются.
Зачем это спрашивают: Функция copy является стандартным способом скопировать элементы среза в заранее выделенное целевое пространство.
Тип ключа map должен поддерживать сравнение на равенство с помощью == и !=.
- Логические значения, числа, строки, указатели, каналы, интерфейсы, массивы и структуры могут быть ключами, если их значения сравнимы.
- Структура сравнима, только если сравнимы все ее поля.
- Срезы, map и функции нельзя использовать как ключи map.
- Каждый ключ определяет не более одного значения в map.
Зачем это спрашивают: Поиск в map требует сравнимых ключей, чтобы Go мог определить равенство двух ключей.
Чтение отсутствующего ключа map возвращает нулевое значение типа значений этой map.
- Прямое чтение имеет форму value := m[key].
- Форма comma-ok value, ok := m[key] также сообщает, существует ли ключ.
- ok равен false для отсутствующего ключа и true для существующего.
- Логическое значение отличает отсутствующий ключ от существующего ключа с нулевым значением.
Зачем это спрашивают: Форма comma-ok устраняет неоднозначность, когда допустимое сохраненное значение может совпадать с нулевым значением типа.
Nil-map можно читать, но перед сохранением элементов ее нужно инициализировать.
- Чтение nil-map возвращает нулевое значение типа значений.
- Присваивание элемента nil-map вызывает панику во время выполнения.
- make(map[K]V) создает инициализированную map, в которую можно записывать элементы.
- delete(m, key) удаляет ключ и безопасен для отсутствующих ключей и nil-map.
Зачем это спрашивают: Для записи map необходимо инициализировать, а чтение и удаление безопасно определены и для nil-map.
Закрытые вопросы
- 21
Что такое структура в Go?
structs - 22
Как работают литералы структур и что является нулевым значением структуры?
structs - 23
Чем метод отличается от функции в Go?
- 24
Что означает получатель-значение у метода Go?
- 25
Что означает получатель-указатель у метода Go?
- 26
Как Go автоматически берёт адрес и разыменовывает значения при вызове методов?
- 27
Что такое указатели, & и * в Go?
- 28
Как в Go передаются аргументы, включая аргументы-указатели?
- 29
Что такое встраивание структур и продвигаемые члены в Go?
structsnlp - 30
Как регистр идентификаторов управляет экспортом в Go?
- 31
Как работают интерфейсы в Go и что означает неявная реализация интерфейса?
typesinterfaces - 32
Почему в Go предпочитают небольшие интерфейсы?
typesinterfaces - 33
Чем nil-интерфейс отличается от интерфейса с типизированным nil-значением?
typesinterfaces - 34
Как в Go работает проверка типа в форме comma-ok?
forms - 35
Что такое переключатель типов в Go?
- 36
Что представляет собой встроенный интерфейс error в Go?
typesinterfaces - 37
Как errors.New и fmt.Errorf создают ошибки в Go?
- 38
Как работают оборачивание ошибок через %w и функция errors.Is?
error-handling - 39
Как errors.As работает вместе с пользовательскими типами ошибок?
error-handling - 40
Как идиоматично проверять и возвращать ошибки в Go?
- 41
В каком порядке выполняются отложенные вызовы и когда вычисляются их аргументы?
decision-makingerror-handling - 42
Для чего в Go используется panic?
error-handling - 43
При каких условиях recover останавливает panic?
error-handling - 44
Как работают пакеты и импорты и чем отличается package main?
- 45
Что определяет go.mod и как семантические версии влияют на пути импорта модулей?
- 46
Для чего нужна функция init и в каком базовом порядке инициализируются пакеты?
- 47
Что такое горутина и что делает инструкция go?
concurrency - 48
Как небуферизованный канал синхронизирует горутины?
concurrency - 49
Как для буферизованных каналов работают емкость, блокировка и ответственность за закрытие?
ownershipcapacityconcurrency - 50
Как работают получение с comma-ok, range по закрытому каналу и select?
concurrency - 51
Функция добавляет элементы в слайс через append, но вызывающий код их не видит. Как это исправить?
slices-maps - 52
Код записывает значение в nil map и падает с паникой. Что вы измените?
error-handling - 53
Вспомогательная функция открывает файл и иногда оставляет файловый дескриптор незакрытым. Как это исправить?
descriptors - 54
Нужно собрать список указателей на значения, полученные в цикле. Как не допустить, чтобы все указатели ссылались на переиспользуемую память?
- 55
Функция принимает интерфейс с типизированным nil-указателем, но проверка if value == nil не срабатывает. Как поступить?
typesinterfaces - 56
Метод должен менять поле структуры, но после вызова значение остаётся прежним. Что вы проверите и исправите?
structs - 57
Нужно скопировать слайс перед изменением, чтобы оригинал остался прежним. Какой код вы используете?
slices-maps - 58
Горутина обновляет map, пока HTTP-обработчик читает её. Как исправить гонку?
concurrencyhttp - 59
Несколько горутин увеличивают общий счётчик запросов, но итоговое значение оказывается меньше ожидаемого. Что вы измените?
concurrency - 60
Программа запускает рабочие горутины, но завершается раньше них. Как дождаться их окончания?
concurrency - 61
Горутина навсегда ждёт отправки результата после тайм-аута вызывающего кода. Как предотвратить утечку?
concurrency - 62
Получатель бесконечно выполняет range по каналу после обработки всех задач. Чего не хватает?
concurrency - 63
Две горутины могут закрыть один канал, и программа иногда падает с паникой. Как изменить схему?
concurrencyerror-handling - 64
Нужно получить результат, но прекратить ожидание через 500 миллисекунд. Как вы это напишете?
- 65
В цикле с select есть ветка default, и теперь в простое он полностью загружает одно ядро CPU. Как это исправить?
- 66
Вы реализуете параллельную обработку задач с фиксированным пределом. Как не запускать отдельную горутину для каждой задачи?
concurrency - 67
Этап конвейера досрочно останавливается из-за ошибки, а предыдущий этап продолжает отправку. Как избежать заблокированных горутин?
concurrencyci-cd - 68
Для исправления дедлока добавили буфер канала, но на больших входных данных код всё равно зависает. Что вы сделаете?
lockingconcurrency - 69
Несколько горутин добавляют ошибки в один слайс. Как безопасно их собрать?
concurrencyslices-maps - 70
Кэш использует RWMutex, но метод захватывает RLock, а затем пытается изменить map. Как это исправить?
caching - 71
Функция возвращает fmt.Errorf("load user: %v", err), после чего errors.Is перестаёт распознавать причину. Что вы измените?
error-handling - 72
Репозиторий возвращает sql.ErrNoRows в HTTP-обработчик. Как обработать это аккуратно?
sqlhttp - 73
Очистка завершилась ошибкой после того, как основная операция уже упала. Как вернуть полезную информацию?
- 74
Функция логирует ошибку и одновременно возвращает её, поэтому каждый слой создаёт одинаковую запись. Какое правило вы введёте?
- 75
HTTP-запрос продолжает выполняться после отключения клиента. Как связать отмену с работой?
resiliencehttp - 76
Вы добавили тайм-аут через context.WithTimeout внутри функции. Какая очистка нужна?
resilienceconcurrency - 77
Фоновая горутина сохраняет контекст запроса и использует его намного позже завершения обработчика. Как изменить решение?
concurrency - 78
JSON-эндпоинт молча принимает неизвестные поля и скрывает опечатки клиентов. Как сделать декодирование строже?
endpoints - 79
JSON-ответ раскрыл хэш пароля, потому что код сериализовал всю структуру пользователя. Что вы измените?
passwordsstructs - 80
В PATCH-запросе нужно различать отсутствующее поле, false и true. Как вы его смоделируете?
- 81
HTTP-обработчик пишет тело ошибки после того, как уже отправил ответ 200. Как его структурировать?
httpstructs - 82
Обработчик читает тело запроса без ограничения и может исчерпать память. Какую защиту вы добавите?
memory - 83
Вызов через HTTP-клиент иногда зависает навсегда. Как сделать его безопасным?
http - 84
Сервис вызывает сторонний API и получает ответ не из диапазона 2xx. Что нужно сделать до декодирования успешных данных?
fan-outapi - 85
Обработчику нужен ID пользователя из пути, но сейчас он доверяет любой строке. Как вы его проверите?
validation - 86
Нужно middleware для записи длительности запроса без нарушения работы обработчика. Как вы его реализуете?
middleware - 87
Табличный модульный тест останавливается на первом неудачном случае. Как улучшить диагностику?
unit - 88
Тесты проходят по отдельности, но падают вместе из-за общей map на уровне пакета. Как их исправить?
testing - 89
Как протестировать HTTP-обработчик без запуска настоящего сервера?
http - 90
Функция напрямую зависит от конкретного клиента базы данных, поэтому её трудно модульно тестировать. Какой небольшой рефакторинг вы сделаете?
refactoringdatabaseunit - 91
Тест сравнивает полный текст ошибки и ломается после добавления контекста. Как сделать его устойчивым?
concurrency - 92
Параллельный тест нестабилен, потому что спит 50 миллисекунд и предполагает, что работа завершилась. Чем заменить sleep?
flakyconcurrency - 93
Как проверить, что функция останавливается при отмене контекста?
concurrency - 94
Функция разбора вызывает панику на некорректных пользовательских данных. Как изменить реализацию и тест?
error-handling - 95
Тесту нужен временный выходной файл. Как не оставить файлы после выполнения?
- 96
Функция запускает горутины в цикле, и каждая должна обработать свой вход. Как сделать это намерение явным и тестируемым?
concurrency - 97
Производитель может не отправить ни одного значения, но потребитель всё равно должен завершиться. Как реализовать и проверить этот крайний случай?
- 98
Эндпоинт возвращает JSON, но при ошибке кодирования иногда отправляет частичный ответ. Как снизить этот риск?
endpoints - 99
Сервис повторяет любой неудачный HTTP-запрос, включая POST и ответы 400. Как сделать повторы безопаснее?
resiliencehttp - 100
Вам достался небольшой Go API с возможными гонками и слабыми тестами. В какой последовательности вы проверите его перед изменением поведения?
ownershipapitesting