СОБЕСЕДОВАНИЕ НА MIDDLE FRONTEND РАЗРАБОТЧИКА. Уничтожение за 6 лет опыта
Сегодня мы разберём собеседование с 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 необходим:
| Сценарий | SSR | CSR |
|---|---|---|
| Маркетинговая страница | ✅ | ❌ |
| Интернет-магазин | ✅ | ❌ |
| Блог / Медиа | ✅ | ❌ |
| Личный кабинет | ❌ | ✅ |
| Админ-панель | ❌ | ✅ |
| Графический редактор | ❌ | ✅ |
| Чат / 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² + n + log n) = O(n²) // n² растёт быстрее всего
Рекомендация:
Для Go-разработчика понимание сложности алгоритмов критически важно, потому что Go часто используется для высоконагруженных систем, где неэффективный алгоритм может стать узким местом. При анализе всегда определяйте переменные (N, M и т.д.) и учитывайте все вложенные операции, включая сортировки, поиск в хеш-таблицах и строковые операции.
