Перейти к основному содержимому

СОБЕСЕДОВАНИЕ НА MIDDLE FRONTEND РАЗРАБОТЧИКА. Уничтожение за 6 лет опыта

· 74 мин. чтения

Сегодня мы разберём собеседование с 21-летним кандидатом Денисом, который позиционирует себя как Middle/Middle+ разработчик с 6-летним опытом программирования. Несмотря на юный возраст и впечатляющий стаж, интервьюер проверяет не заученные ответы из чатов и шпаргалок, а глубину понимания технологий через нестандартные вопросы, практические задачи и разбор типичных пробелов в знаниях.

Вопрос 1. Расскажите о себе и своём опыте работы.

Таймкод: 00:04:42

Ответ собеседника: Правильный. Денис, 21 год. Программированием увлекается с 7 лет, JavaScript — последние 6 лет. Начал с PHP, перешёл на JavaScript. Долгое время разрабатывал Telegram-ботов на фрилансе, затем заинтересовался фронтендом — писал лендинги, веб-приложения, например туду-лист с интеграцией Telegram.

Правильный ответ:

Ответ собеседника дан в правильном формате — кратко, структурировано, с указанием ключевых этапов профессионального пути. Для позиции Go-разработчика было бы полезно дополнить рассказ следующими деталями, если они есть:

Что стоит добавить в рассказ о себе для Go-вакансии:

  • Опыт работы с Go: как давно начал изучать, какие проекты писал, с какими библиотеками и фреймворками работал (gin, echo, gRPC, sqlx и т.д.).
  • Опыт работы с базами данных: PostgreSQL, MySQL, Redis, MongoDB — на каком уровне.
  • Понимание микросервисной архитектуры, контейнеризации (Docker, Kubernetes), CI/CD.
  • Опыт работы в команде, code review, процессы разработки.
  • Конкретные достижения: оптимизация производительности, проектирование API, работа с высоконагруженными системами.

Пример расширенного ответа:

«Меня зовут Денис, мне 21 год. Программированием занимаюсь с 14 лет — начинал с PHP, затем 6 лет активно работал с JavaScript: разрабатывал Telegram-ботов на фрилансе, писал фронтенд-приложения — лендинги, веб-приложения, в том числе todo-лист с интеграцией Telegram API. Около года назад начал изучать Go — писал REST API на Gin, работал с PostgreSQL через pgx, реализовывал микросервисную архитектуру с gRPC. Интересуюсь бэкенд-разработкой, производительностью систем и чистой архитектурой. Сейчас хочу развиваться именно в направлении Go-разработки.»

Такой формат показывает осознанный переход в Go-стек и готовность к работе на позиции бэкенд-разработчика.

Вопрос 2. Какой у вас опыт можно считать серьёзным профессиональным опытом?

Таймкод: 00:05:30

Ответ собеседника: Правильный. Примерно 3 с половиной года. Первоначально на фрилансе делал ботов для Telegram, но не считает тот опыт профессиональным, так как это была копипаста из Google.

Правильный ответ:

Собеседник честно и адекватно оценивает свой опыт — это важное качество. Разберём, как правильно позиционировать опыт на собеседовании.

Что считается профессиональным опытом:

  • Коммерческий опыт — разработка в рамках компании, с дедлайнами, code review, командной работой. Это наиболее ценный тип опыта.
  • Фриланс-опыт — может считаться профессиональным, если кандидат самостоятельно проходил полный цикл: получение требований → проектирование → реализация → деплой → поддержка. Даже если использовались готовые решения, важно показать, что понимаешь, почему выбрано именно это решение.
  • Pet-проекты и open-source — не заменяют коммерческий опыт, но демонстрируют инициативность и глубину знаний.

Как правильно презентовать фриланс-опыт:

Вместо «это была копипаста из Google» лучше сказать:

«На фрилансе я разрабатывал Telegram-ботов под заказ. Использовал готовые библиотеки и примеры, но при этом разбирался в коде, адаптировал решения под конкретные требования клиентов, деплоил и поддерживал проекты. Этот опыт научил меня работать с внешними API, базами данных и деплоем, хотя я понимаю, что командная разработка — это совсем другой уровень.»

Рекомендация для кандидата:

3.5 года опыта — это уровень junior+/middle. Для Go-позиции важно акцентировать внимание на том, что из этого опыта применимо к бэкенд-разработке: работа с API, базами данных, асинхронными задачами, деплоем. Фронтенд-опыт тоже ценен — он даёт понимание полного цикла разработки и взаимодействия между клиентом и сервером.

Вопрос 3. Как вы оцениваете свой уровень разработчика?

Таймкод: 00:06:50

Ответ собеседника: Правильный. На фронтенде оценивает себя как middle, возможно middle plus, на бэкенде — просто middle. Алгоритмы и графы нужно подтянуть, но базовую логику пишет уверенно. В целом уровень middle plus.

Правильный ответ:

Собеседник даёт адекватную и структурированную самооценку. Разберём, как грамотно позиционировать свой уровень на собеседовании.

Как оценивать свой уровень — ориентиры:

Junior:

  • Пишет рабочий код по готовым примерам и документации.
  • Понимает базовые конструкции языка, ООП, основы БД.
  • Нуждается в code review и менторинге.
  • Типичный опыт: 0–1.5 года.

Middle:

  • Самостоятельно решает задачи средней сложности.
  • Понимает архитектурные паттерны, умеет проектировать модули.
  • Работает с базами данных, внешними API, пишет тесты.
  • Понимает жизненный цикл приложения, деплой, мониторинг.
  • Типичный опыт: 2–4 года.

Senior:

  • Проектирует сложные системы, принимает архитектурные решения.
  • Менторит коллег, проводит code review на высоком уровне.
  • Понимает trade-offs между подходами, умеет оценивать риски.
  • Типичный опыт: 5+ лет.

Как правильно презентовать самооценку:

Формулировка собеседника хороша, но для Go-позиции стоит конкретизировать бэкенд-навыки:

«Себя оцениваю как middle разработчика. На фронтенде чувствую себя увереннее — там опыт глубже. В бэкенд-разработке на Go я уверенно работаю с REST API, базами данных, конкурентностью. Понимаю принципы чистой архитектуры, умею писать тесты. Зоны роста — алгоритмы на графах и более глубокое понимание распределённых систем. Но базовую логику и типовые задачи решаю самостоятельно.»

Важно: самооценка должна подтверждаться конкретными примерами из проектов. Если кандидат называет себя middle, он должен на собеседовании продемонстрировать понимание не только «как сделать», но и «почему именно так».

Вопрос 4. Какие у вас зарплатные ожидания?

Таймкод: 00:07:55

Ответ собеседника: Правильный. 3000 евро чистыми — было бы хорошо для комфортной жизни безотказно.

Правильный ответ:

Ответ собеседника прямой и понятный. Разберём, как правильно обсуждать зарплатные ожидания на собеседовании.

Как правильно называть зарплатные ожидания:

  • Уточняйте формат: «чистыми» (на руки) или «до вычета налогов» (gross). В разных странах и компаниях принято обсуждать по-разному. В Европе чаще говорят gross, в России и СНГ — net (на руки).
  • Обосновывайте ожидания: привязывайте к рынку, своему уровню и опыту. Можно упомянуть, что изучили рынок для данной позиции и локации.
  • Демонстрируйте гибкость: покажите, что готовы обсуждать полный пакет — не только зарплату, но и бонусы, опционы, обучение, удалённую работу.

Пример более развёрнутого ответа:

«Мои ожидания — около 3000 евро чистыми. Я изучил рынок для middle Go-разработчика и считаю, что эта сумма соответствует моему уровню и опыту. При этом я открыт к обсуждению полного компенсационного пакета — бонусы, возможность удалённой работы, бюджет на обучение и конференции тоже важны для меня.»

Рекомендации:

  • Перед собеседованием изучите зарплаты на Glassdoor, Levels.fyi, LinkedIn для целевой позиции и локации.
  • Не занижайте ожидания из неуверенности — это может сигнализировать о низкой самооценке.
  • Если компания не может предложить нужную сумму, уточните, есть ли возможность пересмотра через 3–6 месяцев после испытательного срока.

Вопрос 5. Какой вы видите идеальную команду и процессы разработки внутри неё?

Таймкод: 00:08:16

Ответ собеседника: Неполный. Важен адекватный тимлид и окружение — люди, которые могут помочь, подсказать, коммуникабельные, дружелюбные и понимающие свою работу. По поводу процессов затруднился ответить, так как работал в основном на фрилансе в команде, где процесс сводился к разным веткам на Git, pull request и общению в Telegram.

Правильный ответ:

Собеседник верно описал важность командной культуры, но его ответ о процессах недостаточно глубокий для middle-уровня. Разберём, что ожидают услышать.

Командная культура — что важно:

Собеседник правильно отметил: адекватный тимлид, готовность помочь, открытая коммуникация. Это действительно ключевые факторы. Дополнительно стоит упомянуть:

  • Code review как инструмент обмена знаниями, а не только контроля качества.
  • Здоровая обратная связь — возможность открыто обсуждать проблемы и предлагать улучшения.
  • Разделение ответственности — каждый понимает свою зону, но готов помочь коллеге.

Процессы разработки — что знать на middle-уровне:

Даже если опыт преимущественно фрилансный, кандидат должен понимать базовые процессы, принятые в компаниях:

Система контроля версий (Git Flow / Trunk-based):

  • Ветки: main (production), develop (staging), feature/fix-ветки.
  • Pull/Merge Request с обязательным code review.
  • Семантическое версионирование (semver).

CI/CD (Continuous Integration / Continuous Delivery):

  • Автоматические тесты при каждом пуше.
  • Линтин.golangci-lint), проверка формата кода.
  • Автоматический деплой в staging, ручной или автоматический — в production.

Методологии разработки:

  • Scrum: спринты (1–2 недели), планирование, дейли, ретроспектива.
  • Kanban: непрерывный поток, лимиты на задачи в работе.
  • Гибридные подходы (Scrumban).

Мониторинг и наблюдаемость:

  • Логирование (structured logging).
  • Метрики (Prometheus, Grafana).
  • Трейсинг (Jaeger, OpenTelemetry).
  • Алертинг.

Пример полного ответа:

«Идеальная команда для меня — это где есть зрелый тимлид, который может направить, и коллеги, готовые помочь. Важна культура code review, где это не формальность, а обмен знаниями. По процессам — мне комфортно работать по Scrum с двухнедельными спринтами, с понятным планированием и ретроспективой. Я знаком с Git Flow, CI/CD пайплайнами, понимаю важность автоматических тестов и мониторинга. На фрилансе у нас был упрощённый процесс — feature-ветки, pull request, общение в Telegram — но я понимаю, что в компании процессы более зрелые, и готов в них вливаться.»

Такой ответ показывает, что кандидат понимает разницу между фрилансом и работой в компании, и имеет представление о зрелых процессах разработки.

Вопрос 6. Был ли у вас опыт работы в команде с процессами: встречами, дейликами, Scrum?

Таймкод: 00:08:36

Ответ собеседника: Правильный. Опыт работы в команде был только на фрилансе. Процесс отличался только созданием разных веток на Git и pull request. Общались через Telegram, были небольшие дейлики. Если нужно было обсудить новую фичу или спринт — созванивались. Полноценного Scrum с регулярными встречами не было.

Правильный ответ:

Ответ честный и адекватный. Собеседник не придумывает опыт, которого нет, но при этом показывает, что базовые практики ему знакомы.

Что можно улучшить в ответе:

Кандидат мог бы дополнительно показать, что он понимает ценность процессов и готов к более строгому формату:

«Да, мой опыт командной работы — это в основном фриланс-проекты. Мы использовали Git с feature-ветками и pull request, общались в Telegram, проводили короткие дейлики по ситуации. Полноценного Scrum с фиксированными спринтами и регулярными церемониями не было, но я понимаю, зачем эти процессы нужны — для прозрачности, предсказуемости и раннего выявления проблем. Готов работать в рамках структурированных процессов и быстро в них встроиться.»

Рекомендация:

Для middle-позиции важно не просто констатировать отсутствие опыта, а показать понимание процессов и готовность к ним. Интервьюер хочет убедиться, что кандидат не испытает культурного шока при переходе от фриланса к работе в команде с устоявшимися процессами.

Вопрос 7. Почему мы должны выбрать именно вас, а не другого кандидата на эту позицию?

Таймкод: 00:09:13

Ответ собеседника: Неполный. Не дал чёткого ответа — вопрос остался без ответа в предоставленных субтитрах.

Правильный ответ:

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

Структура хорошего ответа на «Почему я?»:

1. Уникальное комбинация навыков: «У меня есть опыт как на фронтенде, так и на бэкенде. Это значит, что я понимаю полный цикл разработки — от интерфейса до серверной логики. Могу эффективнее коммуницировать с фронтенд-командой и проектировать API, которые удобны для клиентской части.»

2. Быстрая обучаемость: «Я начал программировать с 14 лет, прошёл путь от PHP к JavaScript, а затем к Go. Это показывает, что я быстро осваиваю новые технологии и не боюсь выходить из зоны комфорта.»

3. Мотивация и вовлечённость: «Я не просто ищу работу — я целенаправленно хочу развиваться именно в Go-разработке. Изучаю язык, пишу pet-проекты, разбираюсь в экосистеме. Для меня это осознанный карьерный выбор.»

4. Конкретные достижения: «На фрилансе я самостоятельно вёл проекты от общения с клиентом до деплоя. Это развило ответственность и умение доводить задачи до конца без внешнего контроля.»

Пример полного ответа:

«Я думаю, что мне стоит отдать предпочтение, потому что у меня есть несколько преимуществ. Во-первых, у меня full-stack опыт — я понимаю фронтенд и бэкенд, что помогает проектировать более удобные API и лучше коммуницировать с командой. Во-вторых, я быстро учусь — за последний год я освоил Go с нуля и уже могу писать production-ready код. В-третьих, у меня есть опыт самостоятельной работы на фрилансе — я привык брать ответственность за результат и доводить задачи до конца. И наконец, я целенаправленно хочу расти именно в Go-разработке — это не случайный выбор, а осознанное решение.»

Важно: не сравнивать себя с другими кандидатами напрямую («я лучше других»), а фокусироваться на своих сильных сторонах и том, как они подходят для конкретной позиции.

Вопрос 8. Чем вы лучше других кандидатов на эту позицию?

Таймкод: 00:09:15

Ответ собеседника: Неполный. Затруднился ответить, перевёл в шутку про 3000 MMR в Dota 2. Затем предположил, что его преимущество в том, что программирование для него — образ жизни, а не просто работа: в свободное время он делает свои инструменты и проекты.

Правильный ответ:

Шутка помогает снять напряжение, но после неё должен следовать содержательный ответ. Разберём, как правильно презентовать свои преимущества.

Что хорошо в ответе собеседника:

  • Упоминание того, что программирование — это образ жизни и увлечение. Это показывает внутреннюю мотивацию, которая действительно важна для работодателя.
  • Создание собственных инструментов и проектов в свободное время — это признак инициативности и глубокого интереса.

Что нужно добавить:

1. Конкретику вместо абстракций: «Программирование — образ жизни» звучит хорошо, но без примеров это пустая фраза. Нужно привести конкретные примеры:

«В свободное время я написал утилиту для автоматизации деплоя моих проектов на Go. Также разработал Telegram-бота для отслеживания статистики код-ревью. Это показывает, что я не просто пишу код на работе, а постоянно думаю о том, как улучшить процессы.»

2. Уникальные преимущества для позиции:

«У меня есть несколько преимуществ, которые могут быть полезны для вашей команды. Во-первых, у меня есть фронтенд-опыт, что позволяет мне лучше понимать, как потребители будут использовать API, которое я проектирую. Во-вторых, я привык работать самостоятельно — на фрилансе мне приходилось самому принимать решения, деплоить, решать проблемы. В-третьих, я быстро осваиваю новые технологии — за последний год я изучил Go и уже пишу на нём production-ready код.»

3. Связь с потребностями компании:

«Я изучил стек вашей компании и вижу, что вы используете Go, PostgreSQL, Docker. У меня есть опыт работы с этими технологиями, и я быстро встану на боевой путь.»

Рекомендация:

Шумить можно, но после шутки обязательно дайте конкретный, структурированный ответ. Покажите, что вы не просто увлечённый человек, а профессионал, который принесёт измеримую пользу команде.

Вопрос 9. Расскажите про интересный опыт автоматизации или создания инструментов для себя.

Таймкод: 00:10:33

Ответ собеседника: Правильный. Работает в компании, связанной с аджайл-менеджментом, на позиции техподдержки. Вместе с коллегой на базе Puppeteer и SDSA написали браузер, контролируемый их проектом, который выполняет макросы, подставляя необходимые данные. Этот инструмент заменяет 60% рутинных задач, требуется только наблюдение человека. Также написали интерфейс на React и немного Vue (из интереса). Сейчас работают над внедрением ИИ-моделей для автоматизации общения с людьми.

Правильный ответ:

Отличный пример! Собеседник привёл конкретный, измеримый кейс автоматизации. Это именно то, что хотят услышать интервьюеры.

Почему этот ответ хорош:

  • Конкретика: Puppeteer, макросы, браузер под контролем приложения — это технические детали, которые показывают реальный опыт.
  • Измеримый результат: «заменяет 60% задач» — это конкретная метрика эффективности.
  • Инициативность: кандидат не ждал, пока ему скажут автоматизировать — он сам увидел проблему и решил её.
  • Масштабирование: от простой автоматизации к внедрению ИИ — показывает рост и развитие проекта.

Что можно добавить для усиления ответа:

Технические детали:

«Мы использовали Puppeteer для управления браузером и SDSA (вероятно, имелось в виду SAS или другой инструмент) для обработки данных. Инструмент работает так: пользователь задаёт шаблон макроса, система автоматически подставляет данные из базы и выполняет действия в браузере. Мы также сделали интерфейс на React для управления этими макросами — это позволило нетехническим сотрудникам создавать свои сценарии.»

Архитектурные решения:

«Интересным вызовом было обеспечение стабильности — Puppeteer может быть нестабильным при масштабировании. Мы реализовали retry-логику, очередь задач и мониторинг состояния браузера. Также добавили логирование всех действий для отладки и аудита.»

Связь с Go-разработкой:

«Этот опыт научил меня думать об автоматизации и надёжности систем. Сейчас я изучаю Go и вижу, как можно переписать бэкенд этого инструмента на Go — горутины отлично подошли бы для управления множеством параллельных браузерных сессий, а строгая типизация помогла бы избежать ошибок при обработке данных.»

Такой ответ показывает не только прошлый опыт, но и способность мыслить в контексте нового стека.

Вопрос 10. Расскажите про интересную продуктовую задачу по веб-разработке, которую вы решали.

Таймкод: 00:12:04

Ответ собеседника: Неполный. Упомянул проблему с ререндерами в React, изучение Virtual DOM, фикс утечек памяти. Из продуктовых задач — разрабатывал маркетинговую панель для affiliate-партнёрки, в частности систему отслеживания лидов с определением гео через флаги. Затруднился дать конкретный ответ, признавшись что вопрос его запутал.

Правильный ответ:

Собеседник затронул несколько интересных тем, но не смог структурировать ответ. Разберём, как правильно рассказывать о продуктовых задачах.

Структура ответа о продуктовой задаче (STAR-метод):

  • Situation — контекст и проблема.
  • Task — что нужно было сделать.
  • Action — какие действия предпринял.
  • Result — какой результат получил.

Пример полного ответа на основе упомянутого кейса:

«Ситуация: Разрабатывал маркетинговую панель для affiliate-партнёрки. Задача — система отслеживания лидов с определением геолокации пользователя.

Проблема: Нужно было определять страну пользователя и показывать соответствующий контент (флаги, язык, специфические офферы). При этом определение гео должно было работать быстро и не замедлять загрузку страницы.

Решение: Реализовал систему определения геолокации на основе IP-адреса с использованием GeoIP базы. На бэкенде (Node.js/Go) при запросе определялась страна и передавалась клиенту. На фронтенде — кеширование результата в localStorage, чтобы не делать повторные запросы. Для отображения флагов использовал SVG-спрайты для быстрой загрузки.

Технические детали:

// Пример определения геолокации на Go
func GetCountryByIP(ip string, db *geoip2.Reader) (string, error) {
record, err := db.Country(net.ParseIP(ip))
if err != nil {
return "", fmt.Errorf("geoip lookup failed: %w", err)
}
return record.Country.IsoCode, nil
}

// Middleware для добавления страны в контекст
func GeoMiddleware(db *geoip2.Reader) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
country, err := GetCountryByIP(ip, db)
if err != nil {
country = "US" // fallback
}
c.Set("country", country)
c.Next()
}
}

Результат: Система отслеживала лиды с точностью определения гео ~95%. Время определения страны — менее 50мс благодаря кешированию. Конверсия в офферы выросла на 15% благодаря персонализации контента по геолокации.»

Рекомендация:

Всегда готовьте 2–3 кейса из опыта, структурированных по STAR. Это поможет уверенно отвечать на подобные вопросы. Даже если задача кажется простой, важно показать глубину понимания и измеримый результат.

Вопрос 11. Оцените свой уровень TypeScript по 10-балльной шкале и назовите пробелы.

Таймкод: 00:14:38

Ответ собеседника: Правильный. Оценивает уровень как 6 из 10. В последнее время работает только на TypeScript, на чистом JS уже не может писать. Пробелы: нужно изучить Omit, попрактиковаться с дженериками, условные типы (conditional types), infer, а также mapped types.

Правильный ответ:

Собеседник адекватно оценивает свой уровень и правильно идентифицирует зоны роста. Разберём подробнее.

Что соответствует уровню 6/10:

  • Уверенное использование базовых типов, интерфейсов, типизации функций.
  • Понимание дженериков на базовом уровне.
  • Использование утилитарных типов (Partial, Pick, Record и т.д.).
  • Работа с типами в реальных проектах.

Пробелы, которые названы — разберём подробнее:

1. Omit, Pick, Exclude, Extract — утилитарные типы:

// Omit — создаёт тип, исключая указанные свойства
type User = {
id: number;
name: string;
email: string;
password: string;
};

type PublicUser = Omit<User, 'password'>;
// { id: number; name: string; email: string; }

// Pick — создаёт тип, выбирая только указанные свойства
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }

2. Условные типы (Conditional Types):

// Базовый синтаксис: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

// Практический пример: фильтрация типов
type NonNullable<T> = T extends null | undefined ? never : T;

// Распределительные условные типы
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

3. Infer — вывод типов:

// Извлечение типа возвращаемого значения функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = () => string;
type Result = ReturnType<Fn>; // string

// Извлечение типа элемента массива
type ElementType<T> = T extends (infer E)[] ? E : never;
type Numbers = number[];
type Num = ElementType<Numbers>; // number

// Извлечение типа из Promise
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
type A = UnwrapPromise<Promise<string>>; // string

4. Mapped Types:

// Базовый синтаксис
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

type Optional<T> = {
[P in keyof T]?: T[P];
};

// Практический пример: создание типа для API-ответа
type ApiResponse<T> = {
[P in keyof T as `data_${string & P}`]: T[P];
};

type User = { id: number; name: string };
type UserResponse = ApiResponse<User>;
// { data_id: number; data_name: string; }

Что ещё стоит изучить для перехода на 7–8/10:

  • Template Literal Types
  • Discriminated Unions
  • Type Guards и Assertion Functions
  • Module Augmentation
  • Declaration Merging

Рекомендация:

Для Go-разработчика TypeScript — это дополнительный навык, но показывает понимание типизации. Если в проекте есть фронтенд-часть, знание TypeScript будет плюсом. Для бэкенд-позиции на Go это не критично, но демонстрирует широкий кругозор.

Вопрос 12. Какие преимущества даёт TypeScript и почему его важно использовать?

Таймкод: 00:16:22

Ответ собеседника: Неполный. Минимум проблем при деплое, ловит ошибки на этапе написания кода (lifetime ошибки). Строгая типизация (хотя её можно обойти через any). Автоподсказки в IDE — это главная причина, почему полюбил TypeScript. Особенно удобно при работе с NestJS и типами для базы данных — видно, какие поля есть и какие обязательные.

Правильный ответ:

Собеседник назвал несколько важных преимуществ, но ответ можно значительно расширить. Разберём все ключевые преимущества TypeScript.

1. Раннее обнаружение ошибок (Compile-time Error Detection):

TypeScript ловит ошибки на этапе компиляции, а не во время выполнения. Это критически важно для production:

// JavaScript — ошибка обнаружится только в runtime
function getUser(id) {
return users.find(u => u.id === id);
}
getUser("1"); // Ошибка: сравниваем number с string

// TypeScript — ошибка обнаружится сразу
function getUser(id: number): User | undefined {
return users.find(u => u.id === id);
}
getUser("1"); // Ошибка компиляции: Argument of type 'string' is not assignable to parameter of type 'number'

2. Улучшенная разработка (Developer Experience):

  • Автодополнение — IDE знает типы и предлагает правильные свойства и методы.
  • Рефакторинг — безопасное переименование, перемещение кода.
  • Навигация — переход к определению, поиск использований.
  • Документация — типы служат живой документацией кода.

3. Самодокументируемость кода:

// Без типов — непонятно, что принимает функция
function createUser(data) { ... }

// С типами — всё понятно
interface CreateUserDto {
name: string;
email: string;
age?: number; // опциональное поле
}

function createUser(data: CreateUserDto): Promise<User> { ... }

4. Безопасность при рефакторинге:

При изменении интерфейса TypeScript покажет все места, которые нужно обновить:

interface User {
id: number;
name: string;
email: string;
}

// Если переименуем name → fullName, TypeScript покажет все ошибки
const user: User = { id: 1, name: "John", email: "john@example.com" };
console.log(user.fullName); // Ошибка: Property 'fullName' does not exist

5. Лучшая командная работа:

  • Новые разработчики быстрее понимают код благодаря типам.
  • Code review становится проще — типы показывают контракты.
  • Снижается количество багов при интеграции разных модулей.

6. Постепенная типизация (Gradual Typing):

Можно мигрировать проект постепенно — файл за файлом:

// Можно начать с any и постепенно уточнять типы
function processData(data: any): any {
// TODO: добавить типы
return data;
}

// Затем улучшать
interface ProcessDataInput {
id: number;
items: string[];
}

interface ProcessDataOutput {
success: boolean;
processedCount: number;
}

function processData(data: ProcessDataInput): ProcessDataOutput {
return { success: true, processedCount: data.items.length };
}

7. Интеграция с экосистемой:

  • ORM и базы данных — типы для моделей, валидация запросов.
  • API — типизация запросов и ответов, OpenAPI/Swagger.
  • Тестирование — типизация моков и тестовых данных.
  • State management — Redux, Zustand с полной типизация.

Рекомендация:

Для Go-разработчика важно отметить, что TypeScript и Go имеют общую философию — строгая типизация, явность, компиляционная проверка. Go разработчику будет комфортно работать с TypeScript, потому что он уже привык думать о типах и контрактах.

Вопрос 13. Какие антипаттерны в TypeScript вы можете назвать?

Таймкод: 00:17:28

Ответ собеседника: Неполный. Впервые услышал термин «антипаттерны» в контексте TS. Упомянул, что any и @ts-ignore — это плохая практика, их нужно применять только по делу, а не при каждой ошибке. Не смог назвать другие антипаттерны.

Правильный ответ:

Собеседник правильно назвал два основных антипаттерна, но их список значительно шире. Разберём основные антипаттерны в TypeScript.

1. Чрезмерное использование any:

// Плохо — any отключает все проверки
function processData(data: any): any {
return data.value; // Нет проверки, нет автодополнения
}

// Хорошо — используем конкретные типы или unknown
function processData(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String((data as { value: unknown }).value);
}
throw new Error('Invalid data');
}

Когда any допустим: миграция legacy-кода, работа с динамическими данными из внешних источников (но лучше использовать unknown).

2. Злоупотребление @ts-ignore и @ts-nocheck:

// Плохо — подавляет все ошибки в следующей строке
// @ts-ignore
const result = someFunction();

// Хорошо — используем более точные решения
// @ts-expect-error — выдаст ошибку, если строка корректна
const result = someFunction();

@ts-expect-error предпочтительнее — он предупредит, если ошибка исчезнет и директива станет ненужной.

3. Неправильное использование Type Assertions (as):

// Плохо — уверенность в типе без проверки
const data = JSON.parse(response) as User;
data.name.toUpperCase(); // Runtime ошибка, если name отсутствует

// Хорошо — валидация данных
interface User {
name: string;
age: number;
}

function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null
&& 'name' in data && typeof data.name === 'string'
&& 'age' in data && typeof data.age === 'number';
}

const data = JSON.parse(response);
if (isUser(data)) {
data.name.toUpperCase(); // Безопасно
}

4. Избыточная типизация (Type Over-engineering):

// Плохо — TypeScript может вывести тип автоматически
const name: string = "John";
const numbers: number[] = [1, 2, 3];
const isValid: boolean = true;

// Хорошо — доверяем выводу типов
const name = "John";
const numbers = [1, 2, 3];
const isValid = true;

5. Использование ! (Non-null assertion) без необходимости:

// Плохо — утверждаем, что значение не null без проверки
const element = document.getElementById('root')!;
element.addEventListener('click', handler); // Runtime ошибка, если element === null

// Хорошо — проверяем значение
const element = document.getElementById('root');
if (element) {
element.addEventListener('click', handler);
}

6. Неправильное использование Enums:

// Плохо — числовые enum генерируют неожиданный код
enum Status {
Active, // 0
Inactive, // 1
}

// Хорошо — используем const enum или union types
type Status = 'active' | 'inactive';

// Или const enum
const enum Status {
Active = 'active',
Inactive = 'inactive',
}

7. Игнорирование strict mode:

// tsconfig.json — всегда включайте strict
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}

8. Использование object вместо конкретных типов:

// Плохо — слишком общий тип
function process(obj: object) {
// obj.name — ошибка, нет свойства name
}

// Хорошо — конкретный тип
function process(obj: { name: string }) {
console.log(obj.name); // Безопасно
}

9. Мутация объектов вместо создания новых:

// Плохо — мутируем существующий объект
function updateUser(user: User, name: string) {
user.name = name; // Мутация!
}

// Хорошо — создаём новый объект
function updateUser(user: User, name: string): User {
return { ...user, name };
}

10. Неиспользование readonly:

// Плохо — свойства можно изменить
interface Config {
apiUrl: string;
timeout: number;
}

// Хорошо — защищаем от изменений
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}

Рекомендация:

Знание антипаттернов показывает зрелость разработчика. Для Go-позиции это не критично, но демонстрирует понимание качества кода и лучших практик, что переносится и на Go-разработку.

Вопрос 14. Чем unknown отличается от any в TypeScript? Что представляет собой тип never?

Таймкод: 00:18:04

Ответ собеседника: Неполный. Конкретно с этими типами не работал, знает только что при обработке через switch case изначально выдаётся unknown. Про any знает поверхностно. С определением never затруднился. На вопрос, чем unknown отличается от any (в обоих случаях тип неизвестен), ответить не смог.

Правильный ответ:

Это фундаментальные типы TypeScript, и понимание их различий важно для безопасной работы с типами.

Ключевое отличие unknown от any:

any — это «отключение» проверки типов. С any можно делать что угодно, TypeScript не будет проверять.

unknown — это «безопасный any». Перед использованием значение типа unknown нужно привести к конкретному типу.

// any — можно делать что угодно, никаких ошибок
let a: any = "hello";
a.toUpperCase(); // ✅ Работает
a.nonExistent(); // ✅ TypeScript не ругается, но Runtime ошибка
a + 1; // ✅ TypeScript не ругается

// unknown — нужно сначала проверить тип
let u: unknown = "hello";
u.toUpperCase(); // ❌ Ошибка: Object is of type 'unknown'
u.nonExistent(); // ❌ Ошибка: Object is of type 'unknown'

// Правильное использование unknown — с проверкой типа
if (typeof u === 'string') {
u.toUpperCase(); // ✅ Теперь TypeScript знает, что это string
}

Когда использовать unknown:

  • При работе с внешними данными (API, JSON, пользовательский ввод).
  • Когда тип может быть разным, и нужно явно проверить его.
// Практический пример: безопасный парсинг JSON
function parseResponse(json: string): unknown {
return JSON.parse(json);
}

const data = parseResponse(response);

// Безопасная обработка с type guard
interface ApiResponse {
success: boolean;
data: string[];
}

function isApiResponse(value: unknown): value is ApiResponse {
return (
typeof value === 'object' &&
value !== null &&
'success' in value &&
typeof value.success === 'boolean' &&
'data' in value &&
Array.isArray(value.data)
);
}

if (isApiResponse(data)) {
console.log(data.data); // ✅ TypeScript знает тип
}

Тип never — «невозможный тип»:

never представляет значение, которое никогда не возникает. Используется в нескольких контекстах:

1. Функция, которая никогда не возвращает управление:

// Функция, которая всегда бросает ошибку
function throwError(message: string): never {
throw new Error(message);
}

// Бесконечный цикл
function infiniteLoop(): never {
while (true) {
// никогда не завершается
}
}

2. Исчерпывающая проверка (Exhaustive Check):

type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// Если все случаи обработаны, shape имеет тип never
const _exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${_exhaustiveCheck}`);
}
}

// Если добавим новый тип 'rectangle' в Shape, но не обработаем его в switch,
// TypeScript выдаст ошибку в default-ветке, потому что shape не будет never

3. Типы, которые не могут содержать значений:

// Пересечение несовместимых типов
type A = string & number; // never — значение не может быть одновременно string и number

// Фильтрация типов с помощью Exclude
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | null | undefined>; // string

Сравнение any, unknown, never, void:

ТипОписаниеМожно присвоитьМожно использовать без проверки
anyОтключение проверки типовЛюбое значениеДа
unknownБезопасный anyЛюбое значениеНет, нужна проверка типа
neverНевозможный типНичегоНикогда
voidОтсутствие значенияundefinedНет

Рекомендация:

Используйте unknown вместо any везде, где тип неизвестен заранее. Это заставит вас писать безопасный код с явными проверками типов. never — мощный инструмент для исчерпывающих проверок, который помогает не забыть обработать все случаи.

Вопрос 15. Что такое Type Assertion (as) в TypeScript и почему это считается антипаттерном?

Таймкод: 00:20:17

Ответ собеседника: Неполный. Использует as, когда значение может быть null, но он уверен, что оно не будет null. Также при заполнении объекта — передаёт пустой объект и через as указывает нужный тип, чтобы работали автоподсказки. На вопрос, почему это плохо, предположил, что лучше добавить if-проверку. Не смог самому объяснить проблему с точки зрения будущих изменений кода.

Правильный ответ:

Type Assertion (as) — это инструмент, который говорит компилятору: «Я знаю лучше, какой тип у этого значения». Проблема в том, что TypeScript доверяет вам полностью и отключает проверки.

Что такое Type Assertion:

// Type Assertion — говорим TypeScript, что это конкретный тип
const value = fetchData() as User;
const element = document.getElementById('root') as HTMLInputElement;

Почему это антипаттерн:

1. Обход системы типов без проверки:

// Плохо — утверждаем тип без проверки
const data = JSON.parse(response) as User;
data.name.toUpperCase(); // Runtime ошибка, если name отсутствует или не строка

// Хорошо — проверяем данные
function isUser(data: unknown): data is User {
return (
typeof data === 'object' && data !== null &&
'name' in data && typeof data.name === 'string'
);
}

const data = JSON.parse(response);
if (isUser(data)) {
data.name.toUpperCase(); // Безопасно
}

2. Пустой объект с приведением типа:

// Плохо — объект не содержит нужных свойств
const user = {} as User;
user.name; // undefined, но TypeScript думает, что это string

// Хорошо — заполняем объект полностью
const user: User = {
id: 1,
name: '',
email: '',
};

3. Проблема с будущими изменениями:

interface User {
id: number;
name: string;
email: string;
}

// Сейчас работает
function processUser(data: unknown) {
const user = data as User;
console.log(user.name);
}

// Через месяц User изменился — добавили обязательное поле phone
// TypeScript не предупредит, потому что data as User — это утверждение, а не проверка
// Runtime ошибки появятся только в production

Когда as допустим:

1. Работа с DOM, когда вы уверены в типе:

const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d'); // TypeScript знает тип canvas

2. Приведение к более конкретному типу (narrowing):

type Animal = { name: string };
type Dog = Animal & { breed: string };

function handleAnimal(animal: Animal) {
if ('breed' in animal) {
const dog = animal as Dog; // Безопасно, мы проверили наличие breed
console.log(dog.breed);
}
}

3. Работа с внешними библиотеками без типов:

// Библиотека без типов
declare function legacyLib(input: string): any;

const result = legacyLib('data') as { items: string[] };

Альтернативы as:

Type Guards (рекомендуемый подход):

// Вместо as — используем type guard
function isUser(data: unknown): data is User {
return (
typeof data === 'object' && data !== null &&
'id' in data && typeof data.id === 'number' &&
'name' in data && typeof data.name === 'string'
);
}

function processData(data: unknown) {
if (isUser(data)) {
// TypeScript знает, что data — User
console.log(data.name);
}
}

Unknown + проверка:

// Вместо any + as — unknown + проверка
function handleResponse(response: unknown) {
if (typeof response === 'object' && response !== null) {
// Работаем с проверенным типом
}
}

Использование библиотек валидации:

import { z } from 'zod';

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

function parseUser(data: unknown): User {
return UserSchema.parse(data); // Безопасная валидация
}

Резюме:

as — это «ядерная кнопка», которая отключает проверку типов. Используйте его только когда действительно понимаете, что делаете, и нет безопасной альтернативы. Предпочитайте type guards, проверки типов и библиотеки валидации — они обеспечивают безопасность на этапе компиляции и runtime.

Вопрос 16. Был ли у вас опыт с серверным рендерингом (SSR)? Какие преимущества и особенности работы с SSR вы знаете?

Таймкод: 00:22:54

Ответ собеседника: Неполный. Опыт с SSR был только через Next.js — переводил простые лендинги на SSR и разрабатывал маркетинговую панель. Знает, что SSR снимает нагрузку с клиента и даёт преимущества для SEO. Из особенностей работы знает только то, что нельзя использовать хуки при SSR. Считает, что Next.js не подходит для больших приложений, так как сильно нагружает сервер и ест оперативную память.

Правильный ответ:

Собеседник назвал базовые преимущества, но его понимание SSR поверхностное. Разберём тему подробно.

Что такое SSR:

Server-Side Rendering — генерация HTML на сервере для каждого запроса. Браузер получает уже готовый HTML, а не пустую страницу с JavaScript.

Преимущества SSR:

1. SEO (Search Engine Optimization): Поисковые роботы получают готовый HTML с контентом. Это критично для маркетинговых страниц, интернет-магазинов, блогов.

// Без SSR — поисковик видит пустую страницу
<html>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>

// С SSR — поисковик видит полный контент
<html>
<body>
<div id="root">
<h1>Заголовок страницы</h1>
<p>Полный контент для индексации</p>
</div>
<script src="bundle.js"></script>
</body>
</html>

2. Быстрая начальная загрузка (First Contentful Paint): Пользователь видит контент сразу, не ждёт загрузки и выполнения JavaScript.

3. Работа на слабых устройствах: Меньше JavaScript нужно выполнить на клиенте — важно для мобильных устройств с медленным процессором.

4. Социальные сети: При шеринге ссылки в соцсетях (Facebook, Telegram) — мета-теги уже в HTML, превью генерируется корректно.

Недостатки SSR:

1. Нагрузка на сервер: Каждый запрос требует рендеринга — нужно масштабирование.

2. Сложность разработки:

  • Нет доступа к window, document, localStorage на сервере.
  • Нужно учитывать, что код выполняется и на сервере, и на клиенте.
  • Гидратация (hydration) — процесс «оживления» HTML на клиенте.

3. TTFB (Time to First Byte): Серверу нужно время на генерацию HTML, первый байт приходит позже, чем при статическом файле.

Особенности работы с SSR:

1. Жизненный цикл компонентов:

// Next.js — getServerSideProps выполняется на сервере
export async function getServerSideProps(context) {
// Здесь можно обращаться к базе данных, файловой системе
const data = await fetchData();

return {
props: { data }, // Данные передаются в компонент
};
}

// Компонент получает данные как пропсы
function Page({ data }) {
// window и document недоступны при SSR
// Используйте useEffect для клиентского кода
useEffect(() => {
// Этот код выполнится только на клиенте
const width = window.innerWidth;
}, []);

return <div>{data.title}</div>;
}

2. Проблема гидратации:

// Ошибка: разный HTML на сервере и клиенте
function BadComponent() {
// Math.random() вернёт разные значения на сервере и клиенте
return <div>{Math.random()}</div>;
}

// Решение: используем useEffect для клиентского кода
function GoodComponent() {
const [value, setValue] = useState<number | null>(null);

useEffect(() => {
setValue(Math.random());
}, []);

return <div>{value ?? 'Loading...'}</div>;
}

3. Доступ к API:

// Плохо — обращение к клиентскому API на сервере
function Component() {
const [data, setData] = useState(null);

useEffect(() => {
// Этот код не выполнится на сервере
fetch('/api/data').then(r => r.json()).then(setData);
}, []);

return <div>{data?.title ?? 'Loading...'}</div>;
}

// Хорошо — данные загружаются на сервере
export async function getServerSideProps() {
const res = await fetch('http://backend:8080/api/data');
const data = await res.json();

return { props: { data } };
}

Стратегии рендеринга:

СтратегияОписаниеКогда использовать
SSRРендеринг на каждый запросДинамический контент, персонализация
SSGРендеринг при сборкеСтатические страницы, блоги, документация
ISRИнкрементальная регенерацияКонтент обновляется периодически
CSRРендеринг на клиентеДашборды, приватные страницы, SPA

Next.js для больших приложений:

Мнение собеседника о том, что Next.js не подходит для больших приложений, частично верно, но требует уточнений:

  • Next.js App Router (начиная с версии 13+) значительно улучшил производительность.
  • Server Components снижают количество JavaScript на клиенте.
  • Streaming SSR позволяет постепенно отправлять контент.
  • Кеширование и ISR снижают нагрузку на сервер.

Рекомендация:

Для Go-разработчика понимание SSR полезно, потому что бэкенд должен эффективно отдавать данные для SSR. Важно проектировать API так, чтобы они были оптимальны для серверных запросов — минимум запросов, агрегация данных, эффективное кеширование.

Вопрос 17. В каких случаях SSR избыточен и можно обойтись обычным React-приложением (CSR)?

Таймкод: 00:25:49

Ответ собеседования: Неполный. Затруднился ответить самостоятельно. После подсказки интервьюера не сам пришёл к ответу.

Правильный ответ:

Это важный архитектурный вопрос. Знание того, когда использовать SSR, а когда нет, показывает зрелость разработчика.

Когда SSR избыточен — CSR достаточно:

1. Приложения за авторизацией (Authenticated Apps):

// Дашборды, личные кабинеты, админки
// Пользователь авторизуется → видит свой контент
// SEO не нужен — поисковики не индексируют приватные страницы

// Примеры:
// - Панель управления проектом
// - Личный кабинет банка
// - CRM-система
// - Аналитические дашборды

2. Внутренние инструменты (Internal Tools):

// Инструменты для сотрудников компании
// Доступ только по VPN или авторизации
// SEO не требуется, пользователи — только сотрудники

// Примеры:
// - Админка для управления контентом
// - Система учёта задач
// - Инструменты мониторинга
// - Панель модерации

3. Приложения с интерактивной графикой:

// Редакторы, конструкторы, визуализации
// Требуют активного использования WebGL, Canvas, Web Workers
// SSR не даёт преимуществ — контент генерируется динамически

// Примеры:
// - Figma, Canva (графические редакторы)
// - Онлайн-карты (Google Maps, Яндекс.Карты)
// - Видеоредакторы
// - Игры в браузере

4. Реал-тайм приложения:

// Чаты, стриминг, совместное редактирование
// Данные постоянно обновляются через WebSocket
// SSR бессмысленен — контент устаревает мгновенно

// Примеры:
// - Slack, Discord (чаты)
// - Google Docs (совместное редактирование)
// - Торговые терминалы
// - Спортивные трансляции с live-статистикой

5. Простые лендинги без SEO-требований:

// Страница внутри приложения, доступная по прямой ссылке
// Пользователь попадает туда из приложения, а не из поисковика

// Примеры:
// - Страница подтверждения оплаты
// - Страница с результатом теста
// - Временная промо-страница

Когда SSR необходим:

СценарийSSRCSR
Маркетинговая страница
Интернет-магазин
Блог / Медиа
Личный кабинет
Админ-панель
Графический редактор
Чат / Real-time
Документация✅ (SSG)

Гибридный подход:

Современные фреймворки (Next.js, Remix) позволяют комбинировать:

// Next.js — можно выбирать стратегию для каждой страницы

// Маркетинговая страница — SSG (статическая генерация)
export async function getStaticProps() {
return { props: { data }, revalidate: 3600 }; // ISR
}

// Каталог товаров — SSR (динамический)
export async function getServerSideProps() {
const products = await fetchProducts();
return { props: { products } };
}

// Личный кабинет — CSR (клиентский рендеринг)
function Dashboard() {
const { data } = useSWR('/api/user/dashboard');
return <div>{data}</div>;
}

Критерии выбора:

  • Нужен SEO? → SSR или SSG
  • Контент персональный? → CSR или SSR
  • Данные обновляются часто? → CSR или SSR с коротким кешем
  • Важна скорость первой загрузки? → SSR или SSG
  • Сложный интерфейс с интерактивностью? → CSR

Рекомендация:

Для Go-разработчика важно понимать, что выбор стратегии рендеринга влияет на архитектуру бэкенда. При SSR бэкенд должен быть готов обрабатывать больше запросов и эффективно кешировать данные. При CSR бэкенд фокусируется на API и может быть более легковесным.

Вопрос 18. Был ли у вас опыт с оптимизацией приложений? Какие методы оптимизации применяли?

Таймкод: 00:28:27

Ответ собеседника: Неполный. Конвертировал изображения в WebP для сжатия без потери качества. Боролся с ререндерами в React. Переводил GIF-анимации в Lottie-анимации для ускорения загрузки страницы.

Правильный ответ:

Собеседник назвал конкретные примеры оптимизации, но список можно значительно расширить. Разберём основные методы оптимизации веб-приложений.

Оптимизация изображений (что назвал собеседник):

// WebP — современный формат с лучшим сжатием
// Поддержка: Chrome, Firefox, Edge, Safari (с 14 версии)

// Используйте picture для fallback
<picture>
<source srcset="image.webp" type="image/webp" />
<source srcset="image.jpg" type="image/jpeg" />
<img src="image.jpg" alt="Description" />
</picture>

// Lottie вместо GIF — векторные анимации вместо растровых
// GIF: 5-50 МБ → Lottie: 50-500 КБ
import Lottie from 'lottie-react';
import animationData from './animation.json';

function AnimatedIcon() {
return <Lottie animationData={animationData} loop={true} />;
}

Оптимизация ререндеров в React:

// 1. React.memo — мемоизация компонента
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* сложный рендеринг */}</div>;
});

// 2. useMemo — мемоизация вычислений
function Component({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
}, [items, filter]);

return <List items={filteredItems} />;
}

// 3. useCallback — мемоизация функций
function Parent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Функция создаётся только один раз

return <Child onClick={handleClick} />;
}

// 4. Виртуализация списков — рендерим только видимые элементы
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}

Оптимизация загрузки кода:

// 1. Code Splitting — разделение кода на чанки
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}

// 2. Tree Shaking — удаление неиспользуемого кода
// Настройте в webpack/rollup:
// mode: 'production', sideEffects: false

// 3. Динамические импорты
async function loadModule() {
const module = await import('./heavyModule');
module.doSomething();
}

Оптимизация сетевых запросов:

// 1. Кеширование запросов
import useSWR from 'swr';

function UserProfile({ userId }) {
const { data } = useSWR(`/api/users/${userId}`, fetcher, {
revalidateOnFocus: false,
dedupingInterval: 60000, // Не повторять запрос чаще 1 минуты
});
return <div>{data?.name}</div>;
}

// 2. Debounce для поиска
import { useDebounce } from 'use-debounce';

function SearchInput() {
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebounce(query, 300);

useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);

return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// 3. Пагинация и бесконечный скролл
function InfiniteScroll() {
const [page, setPage] = useState(1);
const { data, isLoading } = useSWR(`/api/items?page=${page}`);

return (
<div>
{data?.items.map(item => <Item key={item.id} {...item} />)}
{isLoading && <Loading />}
<button onClick={() => setPage(p => p + 1)}>Load more</button>
</div>
);
}

Оптимизация производительности бэкенда (Go):

// 1. Пулы соединений с базой данных
db, err := sqlx.Connect("postgres", dsn)
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

// 2. Кеширование с Redis
func getUser(ctx context.Context, id int) (*User, error) {
cacheKey := fmt.Sprintf("user:%d", id)

// Проверяем кеш
cached, err := redisClient.Get(ctx, cacheKey).Result()
if err == nil {
var user User
json.Unmarshal([]byte(cached), &user)
return &user, nil
}

// Загружаем из БД
user, err := loadUserFromDB(id)
if err != nil {
return nil, err
}

// Сохраняем в кеш
data, _ := json.Marshal(user)
redisClient.Set(ctx, cacheKey, data, 5*time.Minute)

return user, nil
}

// 3. Worker pool для параллельной обработки
func processItems(items []Item) []Result {
results := make([]Result, len(items))
var wg sync.WaitGroup

// Ограничиваем количество горутин
semaphore := make(chan struct{}, 10)

for i, item := range items {
wg.Add(1)
go func(idx int, it Item) {
defer wg.Done()
semaphore <- struct{}{} // Захватываем
defer func() { <-semaphore }() // Освобождаем

results[idx] = processItem(it)
}(i, item)
}

wg.Wait()
return results
}

Метрики производительности (Core Web Vitals):

МетрикаОписаниеЦелевое значение
LCP (Largest Contentful Paint)Время загрузки основного контента< 2.5 сек
FID (First Input Delay)Время до первого взаимодействия< 100 мс
CLS (Cumulative Layout Shift)Стабильность макета< 0.1

Инструменты анализа:

  • Lighthouse — аудит производительности в Chrome DevTools.
  • WebPageTest — детальный анализ загрузки.
  • React DevTools Profiler — анализ ререндеров.
  • webpack-bundle-analyzer — анализ размера бандла.

Рекомендация:

Для Go-разработчика важно понимать, что оптимизация — это не только фронтенд. Бэкенд-оптимизация (кеширование, пулы соединений, эффективные запросы к БД) часто даёт больший эффект, чем фронтенд-оптимизация. Умение профилировать и находить узкие места — ключевой навык.

Вопрос 19. Применяли ли вы ленивую загрузку компонентов (lazy loading) в React?

Таймкод: 00:28:55

Ответ собеседования: Неполный. Конвертировал изображения в WebP для сжатия без потери качества. Боролся с ререндерами в React через мемоизацию (useCallback, useMemo). Переводил GIF-анимации в Lottie для ускорения загрузки. Применял React Virtual Window (виртуализацию списков) для большого списка объектов, но использовал его через готовую библиотеку. Ленивую загрузку компонентов (React.lazy) не использовал, но знает концепцию — компонент подгружается, когда он попадает на страницу или когда выполняется функция.

Правильный ответ:

Собеседник назвал хорошие методы оптимизации, но не имеет практического опыта с React.lazy. Разберём ленивую загрузку подробно.

Что такое Lazy Loading:

Ленивая загрузка — техника, при которой компонент (или модуль) загружается только тогда, когда он действительно нужен, а не при начальной загрузке приложения.

React.lazy — базовое использование:

import { lazy, Suspense } from 'react';

// Без lazy — компонент загружается сразу
import HeavyComponent from './HeavyComponent';

// С lazy — компонент загружается только при первом рендере
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<div>
<h1>Приложение</h1>
{/* Suspense показывает fallback во время загрузки */}
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}

Lazy Loading с роутингом — самый распространённый кейс:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Каждая страница загружается отдельным чанком
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

Lazy Loading по условию (модальные окна, табы):

import { lazy, Suspense, useState } from 'react';

const HeavyChart = lazy(() => import('./components/HeavyChart'));
const DataGrid = lazy(() => import('./components/DataGrid'));

function Dashboard() {
const [showChart, setShowChart] = useState(false);
const [activeTab, setActiveTab] = useState('table');

return (
<div>
<button onClick={() => setShowChart(true)}>
Показать график
</button>

{/* График загрузится только когда пользователь нажмёт кнопку */}
{showChart && (
<Suspense fallback={<div>Загрузка графика...</div>}>
<HeavyChart />
</Suspense>
)}

{/* Таблица загрузится только при переключении на вкладку */}
{activeTab === 'table' && (
<Suspense fallback={<div>Загрузка таблицы...</div>}>
<DataGrid />
</Suspense>
)}
</div>
);
}

Предзагрузка (Prefetch) — загрузка заранее:

import { lazy, useEffect, useState } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
const [showComponent, setShowComponent] = useState(false);

// Предзагрузка при наведении мыши
const handleMouseEnter = () => {
import('./HeavyComponent'); // Начинаем загрузку заранее
};

return (
<div>
<button
onMouseEnter={handleMouseEnter}
onClick={() => setShowComponent(true)}
>
Показать компонент
</button>

{showComponent && (
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}

Lazy Loading с обработкой ошибок:

import { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<ErrorBoundary fallback={<div>Ошибка загрузки компонента</div>}>
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
</ErrorBoundary>
);
}

Что происходит под капотом:

Без lazy loading:
bundle.js (5 МБ) ─────────────────────────────→ Загрузка → Рендер

С lazy loading:
main.bundle.js (500 КБ) ──────────────────────→ Загрузка → Рендер
home.chunk.js (200 КБ) ──→ При перходе на /
dashboard.chunk.js (1 МБ) ──→ При перходе на /dashboard
settings.chunk.js (300 КБ) ──→ При перходе на /settings

Преимущества:

  • Быстрая начальная загрузка — меньше кода при старте.
  • Экономия трафика — пользователь загружает только то, что использует.
  • Меньше нагрузка на память — неиспользуемые компоненты не в памяти.

Ограничения:

  • React.lazy работает только с default exports.
  • SSR требует дополнительной настройки (loadable-components).
  • Нужно обрабатывать состояние загрузки и ошибки.

Аналогия в Go:

В Go аналог lazy loading — ленивые инициализации:

package main

import (
"sync"
)

type LazyLoader struct {
once sync.Once
resource *HeavyResource
}

func (l *LazyLoader) Get() *HeavyResource {
l.once.Do(func() {
// Инициализация происходит только при первом вызове
l.resource = loadHeavyResource()
})
return l.resource
}

func loadHeavyResource() *HeavyResource {
// Тяжёлая операция: чтение файла, подключение к БД и т.д.
return &HeavyResource{}
}

Рекомендация:

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

Вопрос 20. Объясните разницу между useMemo и useCallback в React.

Таймкод: 00:31:14

Ответ собеседника: Неполный. useMemo мемоизирует JSX-элемент (или JSX-узел), вторым аргументом передаётся список зависимостей, и элемент переотрисовывается при изменении зависимостей. useCallback применяется для функций, а не для элементов — при изменении dependency list функция выполняется заново, а не возвращается кэшированное значение. Признался, что в процессе объяснения запутался.

Правильный ответ:

Собеседник перепутал объяснение useCallback — при изменении зависимостей функция создаётся заново, а не «выполняется заново». Разберём оба хука подробно.

Ключевая разница:

  • useMemo мемоизирует результат вычисления (значение).
  • useCallback мемоизирует саму функцию (ссылку на функцию).

useMemo — мемоизация значений:

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items, filter }) {
const [count, setCount] = useState(0);

// Без useMemo — вычисляется при каждом рендере
const filteredItems = items.filter(item => item.category === filter);

// С useMemo — вычисляется только при изменении items или filter
const filteredItems = useMemo(() => {
console.log('Вычисляю отфильтрованный список');
return items.filter(item => item.category === filter);
}, [items, filter]);

// count изменился → компонент перерендерился
// но filteredItems НЕ пересчитался, потому что items и filter не изменились

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

useCallback — мемоизация функций:

import { useCallback, useState } from 'react';

function Parent() {
const [count, setCount] = useState(0);

// Без useCallback — новая функция создаётся при каждом рендере
const handleClick = () => {
console.log('Clicked');
};

// С useCallback — та же функция возвращается, пока зависимости не изменились
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Пустой массив зависимостей — функция создаётся один раз

// Пример с зависимостью
const handleSubmit = useCallback((data: FormData) => {
submitData({ ...data, userId: currentUserId });
}, [currentUserId]); // Функция пересоздаётся только при изменении currentUserId

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* Child НЕ перерендится при изменении count,
потому что handleClick — та же ссылка */}
<Child onClick={handleClick} />
</div>
);
}

Связь между useMemo и useCallback:

useCallback — это частный случай useMemo:

// Эти два выражения эквивалентны:

const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

const handleClick = useMemo(() => {
return () => {
console.log('Clicked');
};
}, []);

Когда использовать каждый:

useMemo:

  • Тяжёлые вычисления (сортировка, фильтрация, преобразование данных).
  • Создание объектов/массивов, которые передаются как пропсы в мемоизированные компоненты.
// Тяжёлое вычисление
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);

// Создание объекта для пропсов
const config = useMemo(() => ({
theme: 'dark',
locale: 'ru',
userId: user.id
}), [user.id]);

useCallback:

  • Функции, передаваемые в дочерние компоненты (особенно мемоизированные).
  • Функции, используемые в зависимостях других хуков (useEffect).
// Функция для дочернего компонента
const handleDelete = useCallback((id: number) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);

// Функция в useEffect
const fetchData = useCallback(async () => {
const response = await fetch(`/api/data?filter=${filter}`);
const data = await response.json();
setData(data);
}, [filter]);

useEffect(() => {
fetchData();
}, [fetchData]);

Типичные ошибки:

// Ошибка 1: Использование useMemo для простых вычислений
// Плохо — накладные расходы на мемоизацию больше, чем на вычисление
const doubled = useMemo(() => count * 2, [count]);

// Хорошо — простое вычисление без мемоизации
const doubled = count * 2;

// Ошибка 2: Забываем указать зависимости
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, []); // ❌ items не в зависимостях — баг!

// Ошибка 3: Мемоизация без React.memo
const handleClick = useCallback(() => {}, []);

// Без React.memo дочерний компонент всё равно перерендится
// useCallback бесполезен без React.memo на дочернем компоненте

Правильная комбинация:

// Родитель
function Parent() {
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return <MemoizedChild onClick={handleClick} />;
}

// Дочерний компонент обёрнут в React.memo
const MemoizedChild = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});

Резюме:

ХукМемоизируетКогда использовать
useMemoРезультат вычисленияТяжёлые вычисления, создание объектов
useCallbackСсылку на функциюФункции для дочерних компонентов, зависимости useEffect

Рекомендация:

Не нужно оборачивать всё в useMemo и useCallback. Используйте их только когда есть реальная проблема с производительностью. Преждевременная оптимизация усложняет код без видимых преимуществ. Сначала профилируйте, потом оптимизируйте.

Вопрос 21. Как предотвратить ререндер дочернего компонента при рендере родителя, если пропсы не изменились?

Таймкод: 00:32:03

Ответ собеседника: Неправильный. Сначала предложил обернуть дочерний компонент в useMemo, передав value и onClick в массив зависимостей. Интервьюер указал, что onClick пересоздаётся при каждом рендере, и мемоизация не работает. После подсказки понял, что нужно мемоизировать onClick через useCallback, а value (объект) — через useMemo. В итоге осознал, что правильнее использовать React.memo для обёртки дочернего компонента, а useCallback и useMemo — для мемоизации пропсов.

Правильный ответ:

Собеседник в итоге пришёл к правильному решению, но путь был тернистый. Разберём полное решение.

Проблема:

function Parent() {
const [count, setCount] = useState(0);

// При каждом рендере создаётся новая функция
const handleClick = () => {
console.log('Clicked');
};

// При каждом рендере создаётся новый объект
const value = { theme: 'dark', locale: 'ru' };

return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* Child перерендится при каждом изменении count,
потому что handleClick и value — новые ссылки */}
<Child onClick={handleClick} value={value} />
</div>
);
}

Решение — три шага:

Шаг 1: Оборачиваем дочерний компонент в React.memo

// React.memo — HOC, который предотвращает ререндер,
// если пропсы не изменились (shallow comparison)
const Child = React.memo(({ onClick, value }) => {
console.log('Child rendered');
return (
<button onClick={onClick}>
Theme: {value.theme}
</button>
);
});

Шаг 2: Мемоизируем функцию через useCallback

function Parent() {
const [count, setCount] = useState(0);

// useCallback возвращает ту же ссылку, пока зависимости не изменились
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Пустой массив — функция создаётся один раз

return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} value={value} />
</div>
);
}

Шаг 3: Мемоизируем объект через useMemo

function Parent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

// useMemo возвращает ту же ссылку, пока зависимости не изменились
const value = useMemo(() => ({
theme: 'dark',
locale: 'ru'
}), []); // Пустой массив — объект создаётся один раз

return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} value={value} />
</div>
);
}

Полное решение:

import { useState, useCallback, useMemo, memo } from 'react';

// Дочерний компонент обёрнут в memo
const Child = memo(({ onClick, value }) => {
console.log('Child rendered');
return (
<div>
<button onClick={onClick}>Click me</button>
<p>Theme: {value.theme}</p>
<p>Locale: {value.locale}</p>
</div>
);
});

function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');

// Мемоизация функции
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);

// Мемоизация объекта
const config = useMemo(() => ({
theme: 'dark',
locale: 'ru',
userId: 123
}), []);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Name"
/>
{/* Child НЕ перерендится при изменении count или name,
потому что onClick и config — те же ссылки */}
<Child onClick={handleClick} value={config} />
</div>
);
}

Как это работает:

Родитель перерендится (count изменился)

handleClick — та же ссылка (useCallback)

config — тот же объект (useMemo)

React.memo сравнивает пропсы: ссылки не изменились

Child НЕ перерендится ✅

Важные нюансы:

1. React.memo использует shallow comparison:

// Shallow comparison сравнивает примитивы по значению,
// объекты и функции по ссылке

// Примитивы — сравнение по значению
5 === 5 // true
"abc" === "abc" // true

// Объекты — сравнение по ссылке
{ a: 1 } === { a: 1 } // false (разные ссылки)
const obj = { a: 1 };
obj === obj // true (та же ссылка)

2. Можно передать кастомную функцию сравнения:

const Child = memo(
({ onClick, value }) => {
return <button onClick={onClick}>{value.name}</button>;
},
(prevProps, nextProps) => {
// Возвращаем true, если ререндер НЕ нужен
return (
prevProps.onClick === nextProps.onClick &&
prevProps.value.name === nextProps.value.name
);
}
);

3. Антипаттерн — использование useMemo для обёртки компонента:

// Плохо — неправильное использование useMemo
const memoizedChild = useMemo(() => (
<Child onClick={handleClick} value={value} />
), [handleClick, value]);

// Хорошо — React.memo на компоненте
const Child = memo(({ onClick, value }) => {
return <div>{/* ... */}</div>;
});

Рекомендация:

Правильная комбинация: React.memo на дочернем компоненте + useCallback для функций + useMemo для объектов. Это стандартный паттерн оптимизации в React. Для Go-разработчика полезно понимать, что мемоизация — это общая концепция, применимая в любом языке: кеширование результатов для избежания повторных вычислений.

Вопрос 22. Будет ли дочерний компонент перерисовываться без React.memo, если родительский компонент рендерится?

Таймкод: 00:37:46

Ответ собеседования: Неправильный. Предположил, что Virtual DOM сравнит данные и не будет перерисовывать элемент,если данные те же. Это оказалось заблуждением — интервьюер пояснил, что без мемоизации дочерний компонент будет перерисовываться при изменении родителя в любом случае.

Правильный ответ:

Это распространённое заблуждение среди React-разработчиков. Разберём, как на самом деле работает рендеринг в React.

Ключевое понятие: Рендер ≠ Перерисовка DOM

В React нужно различать два процесса:

1. Рендеринг (Render) — вызов функции компонента, создание Virtual DOM дерева. Это лёгкая операция в памяти.

2. Коммит (Commit) — применение изменений к реальному DOM. Это тяжёлая операция.

Как работает рендеринг без React.memo:

function Parent() {
const [count, setCount] = useState(0);

console.log('Parent rendered');

return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child name="John" />
</div>
);
}

function Child({ name }: { name: string }) {
console.log('Child rendered'); // Вызывается при КАЖДОМ рендере Parent
return <div>Hello, {name}</div>;
}

Что происходит при нажатии кнопки:

1. Пользователь нажимает кнопку
2. setCount(count + 1) вызывается
3. React планирует ререндер Parent
4. React вызывает Parent() → создаётся новое Virtual DOM дерево
5. React видит, что Child — дочерний компонент
6. React вызывает Child() → создаётся Virtual DOM для Child
7. React сравнивает Virtual DOM Child с предыдущим
8. Если Virtual DOM изменился → обновляет реальный DOM
9. Если Virtual DOM НЕ изменился → НЕ обновляет реальный DOM

Важно: Шаги 4-6 (вызов функций компонентов) происходят всегда, даже если пропсы не изменились. Это и есть рендеринг.

React.memo предотвращает шаги 4-6:

const Child = memo(({ name }: { name: string }) => {
console.log('Child rendered'); // Вызывается ТОЛЬКО при изменении name
return <div>Hello, {name}</div>;
});

Сравнение с React.memo и без:

Без React.memo:
┌─────────────────────────────────────────────────────────────┐
│ Parent рендерится │
│ ↓ │
│ Child ВСЕГДА рендерится (функция вызывается) │
│ ↓ │
│ React сравнивает Virtual DOM │
│ ↓ │
│ Если изменился → обновляет реальный DOM │
│ Если не изменился → не обновляет реальный DOM │
└─────────────────────────────────────────────────────────────┘

С React.memo:
┌─────────────────────────────────────────────────────────────┐
│ Parent рендерится │
│ ↓ │
│ React.memo проверяет пропсы (shallow comparison) │
│ ↓ │
│ Если пропсы изменились → Child рендерится │
│ Если пропсы НЕ изменились → Child НЕ рендерится ✅ │
└─────────────────────────────────────────────────────────────┘

Пример, демонстрирующий разницу:

import { useState, memo } from 'react';

// Без memo — рендерится при каждом рендере Parent
function NormalChild({ count }: { count: number }) {
console.log('NormalChild rendered');
return <div>Count: {count}</div>;
}

// С memo — рендерится только при изменении count
const MemoChild = memo(function MemoChild({ count }: { count: number }) {
console.log('MemoChild rendered');
return <div>Count: {count}</div>;
});

function Parent() {
const [parentCount, setParentCount] = useState(0);
const [childCount, setChildCount] = useState(0);

return (
<div>
<button onClick={() => setParentCount(c => c + 1)}>
Parent Count: {parentCount}
</button>
<button onClick={() => setChildCount(c => c + 1)}>
Child Count: {childCount}
</button>

{/* Рендерится при нажатии ЛЮБОЙ кнопки */}
<NormalChild count={childCount} />

{/* Рендерится только при нажатии "Child Count" */}
<MemoChild count={childCount} />
</div>
);
}

Когда React.memo не помогает:

const Child = memo(({ onClick, data }) => {
return <button onClick={onClick}>{data.name}</button>;
});

function Parent() {
// ❌ Новая функция при каждом рендере
const handleClick = () => {};

// ❌ Новый объект при каждом рендере
const data = { name: 'John' };

// React.memo не поможет — пропсы всегда новые!
return <Child onClick={handleClick} data={data} />;
}

// ✅ Правильно — мемоизируем пропсы
function Parent() {
const handleClick = useCallback(() => {}, []);
const data = useMemo(() => ({ name: 'John' }), []);

return <Child onClick={handleClick} data={data} />;
}

Когда React.memo избыточен:

// 1. Компонент всегда получает разные пропсы
function Timestamp({ time }: { time: number }) {
return <div>{new Date(time).toLocaleString()}</div>;
}
// Не имеет смысла мемоизировать — time всегда новое

// 2. Компонент очень простой
function Badge({ text }: { text: string }) {
return <span className="badge">{text}</span>;
}
// Стоимость рендера меньше, чем стоимость проверки memo

// 3. Компонент редко перерендится
function Footer() {
return <footer>© 2024</footer>;
}
// Нет смысла мемоизировать — и так редко рендерится

Резюме:

  • Без React.memo: дочерний компонент рендерится (функция вызывается) всегда, когда рендерится родитель. Virtual DOM может не измениться, и реальный DOM не обновится, но сам рендеринг происходит.
  • С React.memo: дочерний компонент рендерится только при изменении пропсов. Это экономит время на вызове функции и создании Virtual DOM.

Рекомендация:

Для Go-разработчика полезно провести аналогию: рендеринг в React — это как вызов функции в Go. Даже если результат не изменился, функция всё равно вызывается. React.memo — это как кеширование результата функции: если входные параметры не изменились, повторный вызов не происходит.

Вопрос 23. Проводили ли вы код-ревью? Как вы понимаете процесс код-ревью?

Таймкод: 00:39:52

Ответ собеседника: Неполный. Код-ревью не проводил. Иногда смотрел код других людей, но не давал советов по его правке, считая, что ещё не дорос до того уровня, чтобы давать такие правки. В команде работали через pull request, при мерж-конфликтах сидели вместе и решали. Формального процесса ревью не было.

Правильный ответ:

Собеседник честно признался в отсутствии опыта код-ревью, но его позиция «не дорос» — это заблуждение, которое стоит развеять.

Что такое код-ревью:

Код-ревью — это процесс проверки кода другими разработчиками перед его слиянием в основную ветку. Это не только поиск ошибок, но и обмен знаниями, улучшение качества кода и поддержание стандартов команды.

Зачем нужно код-ревью:

1. Обнаружение ошибок:

  • Логические ошибки, которые автор не заметил.
  • Пограничные случаи (edge cases).
  • Потенциальные баги и уязвимости.

2. Улучшение качества кода:

  • Читаемость и понятность.
  • Следование стандартам и соглашениям.
  • Устранение дублирования и избыточности.

3. Обмен знаниями:

  • Автор узнаёт альтернативные подходы.
  • Ревьюер узнаёт о новых частях системы.
  • Распространение знаний в команде.

4. Снижение рисков:

  • Один человек не является единственным экспертом в части системы (bus factor).
  • Раннее выявление архитектурных проблем.

Как правильно проводить код-ревью:

Что проверять:

// 1. Корректность
// - Делает ли код то, что задумано?
// - Обработаны ли все пограничные случаи?

// Читаемость
// - Понятны ли имена переменных и функций?
// - Не слишком ли сложная логика?

// Пример плохого кода:
func p(u []m) []m {
var r []m
for i := range u {
if u[i].a > 18 {
r = append(r, u[i])
}
}
return r
}

// Пример хорошего кода:
func filterAdultUsers(users []User) []User {
var adults []User
for _, user := range users {
if user.Age > 18 {
adults = append(adults, user)
}
}
return adults
}

2. Архитектура и дизайн:

  • Соответствует ли код архитектуре проекта?
  • Правильно ли выбраны паттерны?
  • Нет ли избыточной связанности?

3. Производительность:

  • Нет ли неэффективных алгоритмов?
  • Нет ли утечек памяти?
  • Правильно ли используются ресурсы?

4. Безопасность:

  • Валидация входных данных.
  • Защита от SQL-инъекций, XSS и т.д.
  • Правильная обработка секретов.

5. Тесты:

  • Есть ли тесты для новой функциональности?
  • Покрыты ли пограничные случаи?
  • Тесты понятны и поддерживаемы?

Как давать обратную связь:

// Плохо — неконструктивная критика
"Этот код ужасен."

// Хорошо — конструктивная обратная связь
"Можно упростить эту логику, используя map вместо вложенных циклов.
Это улучшит читаемость и производительность."

// Плохо — расплывчатое замечание
"Тут что-то не так."

// Хорошо — конкретное предложение
"В строке 42 возможна паника при nil-указателе.
Добавьте проверку: if user != nil { ... }"

// Плохо — приказ
"Переделай это."

// Хорошо — вопрос
"Как вы думаете, можно ли использовать здесь паттерн Strategy?
Это упростит добавление новых типов в будущем."

Процесс код-ревью в команде:

1. Разработчик создаёт feature-ветку
2. Разработчик отправляет Pull Request (PR)
3. Автоматические проверки (CI): тесты, линтеры, сборка
4. Назначается ревьюер (1-2 человека)
5. Ревьюер проверяет код и оставляет комментарии
6. Разработчик вносит правки
7. Ревьюер одобряет PR
8. Код сливается в main/develop

Инструменты для код-ревью:

  • GitHub Pull Requests — комментарии к строкам, предложения изменений.
  • GitLab Merge Requests — аналогичный функционал.
  • Gerrit — специализированный инструмент для ревью.
  • Phabricator — инструмент от Meta.

Рекомендация для собеседника:

Позиция «не дорос до код-ревью» — заблуждение. Код-ревью — это не только для senior-разработчиков. Даже junior может:

  • Задавать вопросы о непонятных частях кода (это помогает автору увидеть проблемы с читаемостью).
  • Проверять соответствие требованиям.
  • Искать очевидные ошибки и опечатки.
  • Учиться на чужом коде.

Начинать можно с ревью простых изменений: исправления багов, небольшие фичи. Это развивает навык чтения кода и критического мышления.

Вопрос 24. Что вы делали по SEO? Работали ли с Open Graph тегами?

Таймкод: 00:41:13

Ответ собеседника: Неполный. Делал базовое SEO: добавлял ключевые слова, оптимизировал скорость загрузки сайта, использовал семантическую вёрстку. Про Open Graph не сразу вспомнил, но затем подтвердил, что работал с OG-тегами (og:image, og:title, og:description). Знает, что они отображаются в превью при шеринге ссылки в соцсетях.

Правильный ответ:

Собеседник назвал базовые практики, но тема SEO значительно шире. Разберём подробно.

Базовое SEO — что назвал собеседник:

1. Ключевые слова:

<!-- Мета-тег с ключевыми словами (устаревший, но всё ещё используется) -->
<meta name="keywords" content="разработка, Go, бэкенд, API" />

<!-- Основной ключевой заголовок -->
<title>Разработка на Go — Услуги бэкенд-разработки</title>

<!-- Описание страницы -->
<meta name="description" content="Профессиональная разработка на Go. REST API, микросервисы, высоконагруженные системы." />

2. Семантическая вёрстка:

<!-- Плохо — без семантики -->
<div class="header">
<div class="title">Заголовок</div>
<div class="nav">
<div class="link">Главная</div>
</div>
</div>

<!-- Хорошо — семантические теги -->
<header>
<h1>Заголовок</h1>
<nav>
<a href="/">Главная</a>
</nav>
</header>
<main>
<article>
<h2>Заголовок статьи</h2>
<p>Содержание...</p>
</article>
</main>
<footer>
<p>© 2024</p>
</footer>

Open Graph теги:

<head>
<!-- Базовые OG-теги -->
<meta property="og:title" content="Название страницы" />
<meta property="og:description" content="Описание страницы" />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Название сайта" />
<meta property="og:locale" content="ru_RU" />

<!-- Дополнительные теги для изображений -->
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Описание изображения" />

<!-- Twitter Card (отдельный протокол) -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Название" />
<meta name="twitter:description" content="Описание" />
<meta name="twitter:image" content="https://example.com/image.jpg" />
</head>

Расширенное SEO — что ещё важно:

1. Мета-теги:

<head>
<!-- Базовые -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Описание страницы" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://example.com/page" />

<!-- Для мобильных устройств -->
<meta name="theme-color" content="#ffffff" />

<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</head>

2. Структурированные данные (Schema.org / JSON-LD):

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Название компании",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+7-123-456-7890",
"contactType": "customer service"
}
}
</script>

3. Sitemap.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://example.com/about</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

4. robots.txt:

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /private/

Sitemap: https://example.com/sitemap.xml

5. Core Web Vitals — техническое SEO:

// Мониторинг производительности
import { onCLS, onFID, onLCP } from 'web-vitals';

function sendToAnalytics(metric) {
// Отправка метрик в аналитику
console.log(metric.name, metric.value);
}

onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);

6. Микроразметка в HTML:

<!-- Хлебные крошки -->
<nav aria-label="breadcrumb">
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/">
<span itemprop="name">Главная</span>
</a>
<meta itemprop="position" content="1" />
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<span itemprop="name">Текущая страница</span>
<meta itemprop="position" content="2" />
</li>
</ol>
</nav>

<!-- Статья -->
<article itemscope itemtype="https://schema.org/Article">
<h1 itemprop="headline">Заголовок статьи</h1>
<time itemprop="datePublished" datetime="2024-01-15">15 января 2024</time>
<span itemprop="author">Имя автора</span>
<div itemprop="articleBody">
<p>Содержание статьи...</p>
</div>
</article>

SEO для SPA/React приложений:

// React Helmet — управление мета-тегами в React
import { Helmet } from 'react-helmet';

function ProductPage({ product }) {
return (
<>
<Helmet>
<title>{product.name} — Купить в магазине</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.imageUrl} />
<meta property="og:type" content="product" />
<meta property="product:price:amount" content={product.price} />
<meta property="product:price:currency" content="RUB" />
<link rel="canonical" href={`https://example.com/products/${product.id}`} />
</Helmet>

<div>
<h1>{product.name}</h1>
<img src={product.imageUrl} alt={product.name} />
<p>{product.description}</p>
</div>
</>
);
}

Инструменты для проверки SEO:

  • Google Search Console — индексация, ошибки, запросы.
  • Google PageSpeed Insights — скорость загрузки.
  • Lighthouse — аудит в Chrome DevTools.
  • Screaming Frog — краулер для анализа сайта.
  • Ahrefs / SEMrush — анализ ключевых слов и конкурентов.

Рекомендация:

Для Go-разработчика SEO — это дополнительный навык, но понимание важно, если бэкенд генерирует HTML (SSR, шаблоны). Важно уметь правильно формировать мета-теги, Open Graph, структурированные данные на сервере. Также бэкенд должен эффективно отдавать sitemap.xml и robots.txt.

Вопрос 25. Расскажите про ваш опыт с тестами. Можно ли заменить unit-тесты e2e-тестами?

Таймкод: 00:42:27

Ответ собеседника: Неполный. Писал unit-тесты на Jest для функций в проекте с браузерной автоматизацией. Для фронтенда писал e2e-тесты на Playwright — пришёл к ним, когда писал парсеры на фрилансе. Понимает e2e как проверку всей воронки действий пользователя от одной точки до другой. Считает, что e2e-тесты не заменяют unit-тесты — их нужно комбинировать. Тесты на компоненты не писал, планирует изучить Vitest и React Testing Library.

Правильный ответ:

Собеседник правильно понимает, что тесты нужно комбинировать, но его опыт ограничен. Разберём тему тестирования подробно.

Пирамида тестирования:

/ E2E \ ← Мало тестов, медленные, дорогие
/---------\
/ Integration \ ← Среднее количество
/---------------\
/ Unit Tests \ ← Много тестов, быстрые, дешёвые
/-------------------\

Unit-тесты:

// Go — unit-тест с использованием стандартного пакета testing
package calculator

import "testing"

func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -1, -2},
{"mixed numbers", -1, 1, 0},
{"zeros", 0, 0, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}

// Go — тест с моками
package service

import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type MockUserRepository struct {
mock.Mock
}

func (m *MockUserRepository) GetByID(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}

func TestUserService_GetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)

expectedUser := &User{ID: 1, Name: "John"}
mockRepo.On("GetByID", 1).Return(expectedUser, nil)

user, err := service.GetUser(1)

assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockRepo.AssertExpectations(t)
}

Integration-тесты:

// Go — интеграционный тест с реальной базой данных
package repository

import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUserRepository_Create(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}

db := setupTestDB(t)
defer teardownTestDB(t, db)

repo := NewUserRepository(db)

user := &User{
Name: "John",
Email: "john@example.com",
}

err := repo.Create(user)
require.NoError(t, err)
assert.NotZero(t, user.ID)

// Проверяем, что пользователь действительно сохранён
saved, err := repo.GetByID(user.ID)
require.NoError(t, err)
assert.Equal(t, user.Name, saved.Name)
}

E2E-тесты:

// Playwright — e2e тест
import { test, expect } from '@playwright/test';

test('user can complete purchase flow', async ({ page }) => {
// 1. Открываем страницу товара
await page.goto('/products/1');
await expect(page.locator('h1')).toHaveText('Product Name');

// 2. Добавляем в корзину
await page.click('[data-testid="add-to-cart"]');
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');

// 3. Переходим в корзину
await page.click('[data-testid="cart-link"]');
await expect(page).toHaveURL('/cart');

// 4. Оформляем заказ
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="address"]', 'Test Street 123');
await page.click('[data-testid="checkout"]');

// 5. Проверяем успешное оформление
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="order-number"]')).toBeVisible();
});

Можно ли заменить unit-тесты e2e-тестами?

Нет, и вот почему:

ХарактеристикаUnit-тестыE2E-тесты
СкоростьМиллисекундыСекунды-минуты
ИзоляцияТестируют одну функцию/методТестируют всю систему
СтабильностьСтабильныеХрупкие (flaky)
СтоимостьДёшевоДорого
ОтладкаЛегко найти причинуСложно найти причину
ПокрытиеМного тестовМало тестов

Проблемы замены unit e2e:

// Unit-тест — быстро и изолированно
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
price float64
discount float64
expected float64
}{
{100, 10, 90},
{100, 0, 100},
{100, 100, 0},
{0, 10, 0},
{-100, 10, -90}, // Пограничный случай
}

for _, tt := range tests {
result := CalculateDiscount(tt.price, tt.discount)
if result != tt.expected {
t.Errorf("got %f, want %f", result, tt.expected)
}
}
}

// E2E-тест для той же логики — медленно и сложно
// 1. Открыть браузер
// 2. Авторизоваться
// 3. Создать товар с ценой 100
// 4. Применить скидку 10%
// 5. Проверить цену 90
// 6. Повторить для каждого случая...

Рекомендуемое соотношение:

Unit-тесты: 70% — быстрые, изолированные
Integration: 20% — проверка взаимодействия компонентов
E2E-тесты: 10% — критические пользовательские сценарии

Тестирование в Go — инструменты:

// 1. Стандартный testing
func TestSomething(t *testing.T) { ... }

// 2. testify — утверждения и моки
import "github.com/stretchr/testify/assert"
assert.Equal(t, expected, actual)

// 3. gomock — генерация моков
//go:generate mockgen -source=repository.go -destination=mock_repository.go

// 4. sqlmock — мокирование SQL
import "github.com/DATA-DOG/go-sqlmock"

// 5. httptest — тестирование HTTP handlers
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/api/users", nil)
w := httptest.NewRecorder()

handler.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
}

// 6. Table-Driven Tests — идиоматический подход в Go
func TestParseInput(t *testing.T) {
tests := []struct {
input string
expected int
hasError bool
}{
{"123", 123, false},
{"abc", 0, true},
{"", 0, true},
}

for _, tt := range tests {
result, err := ParseInput(tt.input)
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
}
}

Рекомендация:

Для Go-разработчика тестирование — это обязательный навык. Go имеет отличную встроенную поддержку тестирования. Важно уметь писать unit-тесты, использовать интерфейсы для изоляции зависимостей и применять table-driven tests — это идиоматический подход в Go. E2E-тесты — это дополнение, а не замена unit-тестам.

Вопрос 26. Когда тесты нужны, а когда их писать нецелесообразно?

Таймкод: 00:45:58

Ответ собеседования: Неполный. Считал, что тесты нужны всегда абсолютно. Не смог самостоятельно объяснить, когда тесты писать нецелесообразно.

Правильный ответ:

Позиция «тесты нужны всегда» — наивная. В реальной разработке всё зависит от контекста. Разберём, когда тесты необходимы, а когда от них можно отказаться.

Когда тесты необходимы:

1. Критическая бизнес-логика:

// Финансовые расчёты — ошибки стоят денег
func CalculateInterest(principal float64, rate float64, time int) float64 {
return principal * math.Pow(1+rate/100, float64(time))
}

// Тесты обязательны: каждая копейка на счету
func TestCalculateInterest(t *testing.T) {
tests := []struct {
principal float64
rate float64
time int
expected float64
}{
{1000, 5, 1, 1050},
{1000, 5, 2, 1102.5},
{0, 5, 1, 0},
}
// ...
}

2. Публичные API и библиотеки:

// Если ваш код используют другие разработчики — тесты обязательны
package validator

func ValidateEmail(email string) bool {
// Реализация
}

// Тесты — это документация и гарантия контракта
func TestValidateEmail(t *testing.T) {
valid := []string{"test@example.com", "user@domain.org"}
invalid := []string{"not-email", "@example.com", "user@"}

for _, email := range valid {
assert.True(t, ValidateEmail(email), "should be valid: %s", email)
}
for _, email := range invalid {
assert.False(t, ValidateEmail(email), "should be invalid: %s", email)
}
}

3. Код с высокой сложностью:

// Сложные алгоритмы — легко допустить ошибку
func FindShortestPath(graph Graph, start, end Node) []Node {
// Реализация алгоритма Дейкстры
}

// Тесты проверяют корректность алгоритма
func TestFindShortestPath(t *testing.T) {
// Проверка различных графов: пустой, один узел, циклы, отрицательные веса
}

4. Код, который часто меняется (рефакторинг):

// Если вы планируете рефакторить — тесты застрахуют от регрессий
// Без тестов: рефакторинг = страх сломать что-то
// С тестами: рефакторинг = уверенность, что ничего не сломано

5. Командная разработка:

// Когда несколько разработчиков работают с одним кодом
// Тесты предотвращают поломку чужого кода

Когда тесты нецелесообразны:

1. MVP и прототипы:

// Стартап проверяет гипотезу
// Через неделю код может быть полностью переписан
// Тесты замедляют итерации

// Пример: лендинг для проверки спроса
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Coming soon!")
})
http.ListenAndServe(":8080", nil)
}
// Тесты здесь избыточны — код временный

2. Одноразовые скрипты:

// Скрипт для миграции данных, который запустится один раз
func migrateLegacyData() {
// Чтение из старой БД → запись в новую
}
// Тесты не нужны — скрипт выполнится один раз и будет удалён

3. Простые CRUD-операции без логики:

// Простой handler без бизнес-логики
func GetUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := userRepo.GetByID(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
// Тесты здесь дают мало пользы — логика тривиальная

4. Генерируемый код:

//go:generate stringer -type=Status
type Status int

const (
StatusPending Status = iota
StatusActive
StatusInactive
)
// stringer генерирует код — тестировать генератор не нужно

5. Конфигурация и настройки:

// Конфигурация приложения
type Config struct {
Port int `env:"PORT" envDefault:"8080"`
DBURL string `env:"DATABASE_URL" required:"true"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
}
// Тесты на конфигурацию обычно избыточны

Матрица принятия решений:

ФакторТесты нужныТесты не нужны
Жизненный цикл кодаДолгийКороткий (MVP)
Количество разработчиковМногоОдин
Критичность ошибкиВысокаяНизкая
Сложность логикиВысокаяНизкая
Частота измененийЧастаяРедкая
Публичный APIДаНет

Экономическая перспектива:

Стоимость тестов = время написания + время поддержки
Выгода тестов = предотвращённые баги + уверенность при рефакторинге + документация

Если Стоимость > Выгода → тесты нецелесообразны
Если Выгода > Стоимость → тесты необходимы

Практический подход:

// 1. Начинайте с тестирования критичной бизнес-логики
// 2. Добавляйте тесты при обнаружении багов (regression tests)
// 3. Увеличивайте покрытие при рефакторинге
// 4. Не гонитесь за 100% покрытием — это дорого и часто бессмысленно

// Пример: сначала код без тестов
func ProcessOrder(order Order) error {
// ...
}

// Потом — тест на найденный баг
func TestProcessOrder_InsufficientFunds(t *testing.T) {
order := Order{Amount: 1000, UserBalance: 500}
err := ProcessOrder(order)
assert.Equal(t, ErrInsufficientFunds, err)
}

Рекомендация:

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

Вопрос 27. Что такое useEffect в React? Какие задачи он решает? Когда его правильно использовать?

Таймкод: 00:48:05

Ответ собеседования: Неполный. useEffect принимает функцию и список зависимостей. Функция выполняется при изменении значений из списка зависимостей. Сначала перепутал поведение с пустым массивом и без второго аргумента, но затем сам исправился. Использует useEffect в основном для запросов на API при монтировании компонента. Не смог чётко объяснить, какие задачи решать в нём не стоит. На вопрос про отписку от слушателей событий — правильно ответил про removeEventListener.

Правильный ответ:

useEffect — один из самых важных и одновременно самых неправильно используемых хуков в React. Разберём его подробно.

Синтаксис и поведение:

useEffect(() => {
// Эффект, который выполняется

return () => {
// Cleanup функция (опционально)
// Вызывается перед следующим выполнением эффекта и при размонтировании
};
}, [dependency1, dependency2]); // Массив зависимостей

Три варианта массива зависимостей:

// 1. Пустой массив [] — выполняется один раз при монтировании
useEffect(() => {
console.log('Компонент смонтирован');
fetchData();
}, []);

// 2. Без второго аргумента — выполняется при КАЖДОМ рендере
useEffect(() => {
console.log('Компонент отрендерился');
// ВНИМАНИЕ: вызывается при каждом изменении любого состояния!
});

// 3. С зависимостями — выполняется при изменении указанных значений
const [userId, setUserId] = useState(1);
useEffect(() => {
console.log('userId изменился:', userId);
fetchUser(userId);
}, [userId]); // Вызывается только при изменении userId

Задачи, которые решает useEffect:

1. Загрузка данных при монтировании:

function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
let cancelled = false; // Флаг для предотвращения обновления размонтированного компонента

async function loadUser() {
setLoading(true);
try {
const data = await fetchUser(userId);
if (!cancelled) {
setUser(data);
}
} catch (error) {
if (!cancelled) {
console.error(error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}

loadUser();

return () => {
cancelled = true; // Отменяем обновление при размонтировании
};
}, [userId]);

if (loading) return <Loading />;
if (!user) return <NotFound />;

return <div>{user.name}</div>;
}

2. Подписка на события с отпиской (cleanup):

function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });

useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}

// Подписка
window.addEventListener('resize', handleResize);

// Cleanup — отписка
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return <div>{size.width} x {size.height}</div>;
}

3. WebSocket соединение:

function Chat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);

useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/chat/${roomId}`);

ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

// Cleanup — закрываем соединение
return () => {
ws.close();
};
}, [roomId]); // Переподключаемся при смене комнаты

return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}

4. Таймеры и интервалы:

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

return <div>Seconds: {seconds}</div>;
}

5. Синхронизация с внешними системами:

function DocumentTitle({ title }: { title: string }) {
useEffect(() => {
document.title = title;

return () => {
document.title = 'Default Title';
};
}, [title]);

return null;
}

Когда НЕ использовать useEffect:

1. Преобразование данных для рендеринга — используйте useMemo:

// Плохо — лишний рендер из-за useEffect + useState
function Component({ items }) {
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);

return <List items={filteredItems} />;
}

// Хорошо — useMemo без лишнего рендера
function Component({ items }) {
const filteredItems = useMemo(
() => items.filter(i => i.active),
[items]
);

return <List items={filteredItems} />;
}

2. Обработка событий пользователя — используйте обработчики:

// Плохо
function Component() {
useEffect(() => {
if (count > 10) {
alert('Count exceeded 10!');
}
}, [count]);

return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

// Хорошо
function Component() {
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
if (newCount > 10) {
alert('Count exceeded 10!');
}
};

return <button onClick={handleClick}>+1</button>;
}

3. Вычисляемые значения — вычисляйте напрямую:

// Плохо
function Component({ price, quantity }) {
const [total, setTotal] = useState(0);

useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);

return <div>{total}</div>;
}

// Хорошо
function Component({ price, quantity }) {
const total = price * quantity;
return <div>{total}</div>;
}

4. Инициализация состояния — используйте начальное значение или lazy initializer:

// Плохо
function Component({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Первый рендер — user === null, потом загрузка, потом данные
// Лишний рендер!
}

// Хорошо — если данные доступны синхронно
function Component({ initialUser }) {
const [user, setUser] = useState(initialUser);
// Нет лишнего рендера
}

// Или используйте библиотеку для загрузки данных
function Component({ userId }) {
const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
// useSWR управляет состоянием загрузки
}

Правила использования useEffect:

// ✅ Используйте для:
// - Загрузки данных с сервера
// - Подписки на события (WebSocket, EventSource)
// - Работы с DOM (фокус, скролл)
// - Таймеров и интервалов
// - Синхронизации с внешними системами

// ❌ Не используйте для:
// - Преобразования данных (useMemo)
// - Обработки событий пользователя (обработчики)
// - Вычисляемых значений (вычисляйте напрямую)
// - Инициализации состояния (начальное значение)

Проблема stale closure:

function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
// count всегда 0 — замыкание на первом значении!
console.log('Count:', count);
setCount(count + 1); // Всегда 0 + 1 = 1
}, 1000);

return () => clearInterval(interval);
}, []); // Пустой массив зависимостей

return <div>{count}</div>;
}

// Решение 1: Используем функциональный updater
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // Получаем актуальное значение
}, 1000);
return () => clearInterval(interval);
}, []);

// Решение 2: Добавляем count в зависимости
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // Пересоздаётся при каждом изменении count

Рекомендация:

useEffect — это инструмент для синхронизации компонента с внешним миром. Если вы не взаимодействуете с чем-то вне React (API, DOM, таймеры, подписки) — скорее всего, useEffect вам не нужен. Чрезмерное использование useEffect — один из самых распространённых антипаттернов в React.

Вопрос 28. Напишите функцию, принимающую массив промисов. Если есть хотя бы один зарезолвленный — вернуть его значение. Если все отклонены — собрать все ошибки в массив и выбросить ошибку с этим массивом.

Таймкод: 00:54:17

Ответ собеседования: Неполный. Понял условие задачи после уточнений. Использовал Promise.allSettled для прохода по всем промисам. Объявил переменные isAnyError и lastError для отслеживания ошибок, а также result для хранения результата. Логика была в целом правильной, но код получился избыточным. После подсказок интервьюера оптимизировал решение.

Правильный ответ:

Разберём задачу и несколько вариантов решения — от простого к оптимальному.

Анализ задачи:

Нужна функция с поведением, похожим на Promise.race, но с отличием: если все промисы отклонены, нужно собрать все ошибки и выбросить их вместе.

Вариант 1: Использование Promise.allSettled (простой):

async function firstSuccessOrAllErrors<T>(promises: Promise<T>[]): Promise<T> {
const results = await Promise.allSettled(promises);

// Ищем первый успешный результат
for (const result of results) {
if (result.status === 'fulfilled') {
return result.value;
}
}

// Все отклонены — собираем ошибки
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason);

throw new AggregateError(errors, 'All promises were rejected');
}

// Использование
const promises = [
fetch('/api/primary'),
fetch('/api/fallback-1'),
fetch('/api/fallback-2'),
];

try {
const result = await firstSuccessOrAllErrors(promises);
console.log('Success:', result);
} catch (error) {
if (error instanceof AggregateError) {
console.log('All failed with errors:', error.errors);
}
}

Вариант 2: Оптимизированный — ранний возврат при первом успехе:

async function firstSuccessOrAllErrors<T>(promises: Promise<T>[]): Promise<T> {
let pendingCount = promises.length;
const errors: Error[] = [];

return new Promise<T>((resolve, reject) => {
promises.forEach(promise => {
promise
.then(value => {
resolve(value); // Ранний возврат при первом успехе
})
.catch(error => {
errors.push(error);
pendingCount--;

if (pendingCount === 0) {
reject(new AggregateError(errors, 'All promises rejected'));
}
});
});
});
}

Вариант 3: С таймаутом (расширенный):

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${ms}ms`));
}, ms);

promise
.then(resolve)
.catch(reject)
.finally(() => clearTimeout(timer));
});
}

async function firstSuccessOrAllErrors<T>(
promises: Promise<T>[],
timeout?: number
): Promise<T> {
const wrappedPromises = timeout
? promises.map(p => withTimeout(p, timeout))
: promises;

const results = await Promise.allSettled(wrappedPromises);

for (const result of results) {
if (result.status === 'fulfilled') {
return result.value;
}
}

const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason);

throw new AggregateError(errors, 'All promises were rejected');
}

Вариант 4: С ограничением конкурентности:

async function firstSuccessOrAllErrorsBatched<T>(
promises: Promise<T>[],
batchSize: number = 3
): Promise<T> {
const errors: Error[] = [];

// Обрабатываем промисы батчами
for (let i = 0; i < promises.length; i += batchSize) {
const batch = promises.slice(i, i + batchSize);
const results = await Promise.allSettled(batch);

// Проверяем успешные результаты в текущем батче
for (const result of results) {
if (result.status === 'fulfilled') {
return result.value;
}
errors.push(result.reason);
}
}

throw new AggregateError(errors, 'All promises were rejected');
}

Тестирование:

// Тест 1: Есть успешный промис
async function test1() {
const promises = [
Promise.reject(new Error('Error 1')),
Promise.resolve('Success!'),
Promise.reject(new Error('Error 2')),
];

const result = await firstSuccessOrAllErrors(promises);
console.log(result); // 'Success!'
}

// Тест 2: Все отклонены
async function test2() {
const promises = [
Promise.reject(new Error('Error 1')),
Promise.reject(new Error('Error 2')),
Promise.reject(new Error('Error 3')),
];

try {
await firstSuccessOrAllErrors(promises);
} catch (error) {
if (error instanceof AggregateError) {
console.log(error.errors.length); // 3
console.log(error.errors[0].message); // 'Error 1'
}
}
}

// Тест 3: Пустой массив
async function test3() {
try {
await firstSuccessOrAllErrors([]);
} catch (error) {
if (error instanceof AggregateError) {
console.log(error.errors.length); // 0
}
}
}

Сравнение подходов:

ПодходПлюсыМинусы
Promise.allSettledПростой, читаемыйЖдёт завершения всех промисов
Ранний возвратБыстрее при первом успехеСложнее в реализации
С таймаутомЗащита от зависанияДополнительная сложность
БатчамиКонтроль нагрузкиМедленнее при раннем успехе

Рекомендация:

Для собеседования лучше всего подходит Вариант 1 с Promise.allSettled — он простой, читаемый и использует стандартные возможности JavaScript. Важно знать про AggregateError — это стандартный способ представления нескольких ошибок в JavaScript.

Вопрос 29. Сгруппировать числа по набору цифр (анаграммы чисел).

Таймкод: 01:06:47

Ответ собеседования: Правильный. Идея решения: использовать Map, где ключом является отсортированный набор цифр числа. Для каждого числа преобразует его в строку, разбивает на массив символов, сортирует и соединяет обратно. Решение заработало корректно, но с небольшими ошибками, которые были исправлены.

Правильный ответ:

Задача на группировку анаграмм — классическая задача на использование хеш-таблиц. Разберём решение подробно.

Решение на JavaScript:

function groupByDigits(numbers: number[]): number[][] {
const groups = new Map<string, number[]>();

for (const num of numbers) {
// Создаём ключ: сортируем цифры числа
const key = String(num).split('').sort().join('');

// Добавляем число в соответствующую группу
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(num);
}

// Возвращаем только группы (значения Map)
return Array.from(groups.values());
}

// Примеры использования
console.log(groupByDigits([3021, 2301, 1230, 456, 645, 546]));
// [[3021, 2301, 1230], [456, 645, 546]]

console.log(groupByDigits([111, 111, 222, 111]));
// [[111, 111, 111], [222]]

console.log(groupByDigits([1, 10, 100, 1000]));
// [[1], [10], [100], [1000]] — разные наборы цифр

Решение на Go:

package main

import (
"fmt"
"sort"
"strconv"
"strings"
)

func groupByDigits(numbers []int) [][]int {
groups := make(map[string][]int)

for _, num := range numbers {
key := sortedDigitsKey(num)
groups[key] = append(groups[key], num)
}

// Собираем результат
result := make([][]int, 0, len(groups))
for _, group := range groups {
result = append(result, group)
}

return result
}

func sortedDigitsKey(num int) string {
digits := strings.Split(strconv.Itoa(num), "")
sort.Strings(digits)
return strings.Join(digits, "")
}

func main() {
fmt.Println(groupByDigits([]int{3021, 2301, 1230, 456, 645, 546}))
// [[3021 2301 1230] [456 645 546]]
}

Оптимизированное решение на Go (без строк):

func groupByDigitsOptimized(numbers []int) [][]int {
groups := make(map[[10]int][]int) // Используем массив как ключ

for _, num := range numbers {
key := digitCountKey(num)
groups[key] = append(groups[key], num)
}

result := make([][]int, 0, len(groups))
for _, group := range groups {
result = append(result, group)
}

return result
}

// Создаём ключ на основе количества каждой цифры
func digitCountKey(num int) [10]int {
var counts [10]int
for num > 0 {
counts[num%10]++
num /= 10
}
return counts
}

// Это эффективнее, чем сортировка строк:
// - Сортировка: O(d * log(d)) где d — количество цифр
// - Подсчёт: O(d) — линейное время

Расширенная версия с фильтрацией:

interface GroupResult {
groups: number[][];
largestGroup: number[];
totalGroups: number;
}

function groupByDigitsDetailed(numbers: number[]): GroupResult {
const groups = new Map<string, number[]>();

for (const num of numbers) {
const key = String(num).split('').sort().join('');
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(num);
}

const allGroups = Array.from(groups.values());
const largestGroup = allGroups.reduce((max, group) =>
group.length > max.length ? group : max
, allGroups[0]);

return {
groups: allGroups,
largestGroup,
totalGroups: allGroups.length,
};
}

// Использование
const result = groupByDigitsDetailed([3021, 2301, 1230, 456, 645, 546, 789]);
console.log(result);
// {
// groups: [[3021, 2301, 1230], [456, 645], [789]],
// largestGroup: [3021, 2301, 1230],
// totalGroups: 3
// }

Анализ сложности:

ПодходВремяПамять
Сортировка строкO(n * d * log(d))O(n * d)
Подсчёт цифрO(n * d)O(n * d)

Где n — количество чисел, d — среднее количество цифр в числе.

Краевые случаи:

function groupByDigitsSafe(numbers: number[]): number[][] {
if (!numbers || numbers.length === 0) {
return [];
}

const groups = new Map<string, number[]>();

for (const num of numbers) {
// Обрабатываем отрицательные числа
const absNum = Math.abs(num);
const key = String(absNum).split('').sort().join('');

if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(num);
}

return Array.from(groups.values());
}

// Тесты краевых случаев
console.log(groupByDigitsSafe([])); // []
console.log(groupByDigitsSafe([0])); // [[0]]
console.log(groupByDigitsSafe([1, 10, 100])); // [[1], [10], [100]]
console.log(groupByDigitsSafe([-123, 321, 132])); // [[-123, 321, 132]]

Рекомендация:

При решении задач на группировку анаграмм ключевая идея — создать каноническую форму ключа. Для чисел это отсортированные цифры или подсчёт каждой цифры. В Go массивы можно использовать как ключи map, что делает решение более эффективным по сравнению со строками.

Вопрос 30. Оцените сложность алгоритма группировки чисел по цифрам в O-нотации.

Таймкод: 01:17:46

Ответ собеседования: Неполный. Предположил, что сложность O(N) из-за цикла. После наводящих вопросов понял, что сортировка добавляет сложность. Не сам пришёл к окончательному ответу.

Правильный ответ:

Анализ сложности — важный навык для любого разработчика. Разберём эту задачу подробно.

Реализация для анализа:

function groupByDigits(numbers) {
const groups = new Map();

for (const num of numbers) { // O(N)
const key = String(num) // O(M) — преобразование в строку
.split('') // O(M) — разбиение на символы
.sort() // O(M log M) — сортировка
.join(''); // O(M) — объединение

if (!groups.has(key)) { // O(1) — хеш-таблица
groups.set(key, []); // O(1)
}
groups.get(key).push(num); // O(1) — амортизированное
}

return Array.from(groups.values()); // O(K), где K — количество групп
}

Пошаговый анализ:

1. Внешний цикл: O(N)

for (const num of numbers) { // Итерируемся по всем N числам

2. Преобразование числа в строку: O(M)

String(num) // Проходим по всем M цифрам числа

3. Разбиение на символы: O(M)

.split('') // Создаём массив из M символов

4. Сортировка: O(M log M)

.sort() // Сортируем M символов — обычно O(M log M)

5. Объединение: O(M)

.join('') // Собираем M символов обратно в строку

6. Операции с Map: O(1)

groups.has(key) // O(1) — хеш-поиск
groups.set(key, []) // O(1) — вставка
groups.get(key).push(num) // O(1) — амортизированное

Итоговая сложность:

O(N × (M + M + M log M + M + 1 + 1 + 1))
= O(N × (4M + M log M + 3))
= O(N × M log M)

Где:

  • N — количество чисел в массиве
  • M — среднее количество цифр в числе (длина числа)

Важные замечания о M:

Для 32-битного целого числа максимум 10 цифр (2^31 - 1 = 2147483647). Для 64-битного — максимум 19 цифр.

Если M ограничено константой (например, M ≤ 19), то M log M тоже константа, и сложность упрощается до O(N).

Сравнение подходов:

// Подход 1: Сортировка — O(N × M log M)
function groupBySorting(numbers) {
const groups = new Map();
for (const num of numbers) {
const key = String(num).split('').sort().join('');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(num);
}
return Array.from(groups.values());
}

// Подход 2: Подсчёт цифр — O(N × M)
function groupByCounting(numbers) {
const groups = new Map();
for (const num of numbers) {
const counts = new Array(10).fill(0);
let n = Math.abs(num);
do {
counts[n % 10]++;
n = Math.floor(n / 10);
} while (n > 0);
const key = counts.join(',');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(num);
}
return Array.from(groups.values());
}
ПодходСложностьПлюсыМинусы
СортировкаO(N × M log M)Простой кодМедленнее на длинных числах
ПодсчётO(N × M)БыстрееСложнее код

Оптимизация в Go:

// Используем массив как ключ — O(N × M) без сортировки
func groupByDigitsOptimized(numbers []int) [][]int {
groups := make(map[[10]int][]int)

for _, num := range numbers { // O(N)
var key [10]int
n := abs(num)
for n > 0 { // O(M)
key[n%10]++
n /= 10
}
groups[key] = append(groups[key], num)
}

result := make([][]int, 0, len(groups))
for _, group := range groups {
result = append(result, group)
}
return result
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

Как оценивать сложность самостоятельно:

Правило 1: Посчитайте вложенные циклы

for (let i = 0; i < n; i++) { // O(n)
for (let j = 0; j < n; j++) { // O(n)
// O(1) операция
}
}
// Итого: O(n × n) = O(n²)

Правило 2: Последовательные операции складываются

for (let i = 0; i < n; i++) { } // O(n)
for (let i = 0; i < n; i++) { } // O(n)
// Итого: O(n + n) = O(n)

Правило 3: Вложенные операции перемножаются

for (let i = 0; i < n; i++) { // O(n)
for (let j = 0; j < m; j++) { // O(m)
// O(1) операция
}
}
// Итого: O(n × m)

Правило 4: Доминирующий член

O(+ n + log n) = O() // n² растёт быстрее всего

Рекомендация:

Для Go-разработчика понимание сложности алгоритмов критически важно, потому что Go часто используется для высоконагруженных систем, где неэффективный алгоритм может стать узким местом. При анализе всегда определяйте переменные (N, M и т.д.) и учитывайте все вложенные операции, включая сортировки, поиск в хеш-таблицах и строковые операции.