СОБЕСЕДОВАНИЕ НА FRONTEND РАЗРАБОТЧИКА ЗП 250К. LIVE CODING
Сегодня мы разберём пример качественного технического собеседования на позицию Middle+/Senior Frontend-разработчика, проведённого в спокойной и дружеской атмосфере, где кандидат продемонстрировал уверенные знания по ключевым фронтенд-технологиям: от работы браузера, DNS, HTTP и CORS до глубокого понимания Event Loop, особенностей React, TypeScript и подходов к управлению состоянием. Помимо теоретических вопросов, кандидат успешно справился с практическими задачами — реализацией debounce, обходом древовидной структуры и рефакторингом React-кода с типичными ошибками, что позволило оценить не только его техническую грамотность, но и умение применять лучшие практики разработки.
Вопрос 1. Расскажите о своём опыте работы и основных технологиях, с которыми вы работали.
Таймкод: 00:05:49
Ответ собеседника: Правильный. Коммерческий опыт работы около трёх лет как в компаниях, так и на проектной работе и стартапах. Основной стек: React, Redux. Также есть опыт работы с Vue около года. Имеется опыт разработки бэкенда на Node.js и Java (Spring Enterprise Edition). Работал как full-stack разработчик на разных проектах.
Правильный ответ:
Ответ собеседника корректен и даёт общее представление об опыте. Для позиции Golang-разработчика важно было бы дополнительно раскрыть следующие моменты:
Акцент на Golang-опыте. Если есть опыт работы с Go, необходимо указать продолжительность, типы проектов (микросервисы, CLI-утилиты, высоконагруженные системы), используемые фреймворки (gin, echo, fiber, chi) и библиотеки.
Базы данных и интеграции. Стоит упомянуть опыт работы с PostgreSQL, MySQL, Redis, Kafka, gRPC, REST API — это ключевые технологии в экосистеме Go.
Инфраструктура и DevOps. Опыт с Docker, Kubernetes, CI/CD, мониторинг (Prometheus, Grafana) будет плюсом.
Архитектурные паттерны. Микросервисная архитектура, event-driven подход, CQRS, работа с очередями сообщений.
Ответ в целом правильный, но для вакансии Golang-разработчика желательно было бы услышать больше деталей именно о бэкенд-опыте и работе с Go-экосистемой.
Вопрос 2. Есть ли проект или сложная задача, которой вы гордитесь?
Таймкод: 00:07:00
Ответ собеседника: Правильный. Последний проект — стартап, который вёл практически самостоятельно от начала до конца в течение года. Было реализовано множество задач разного уровня сложности. К сожалению, проект не был доведён до конца из-за финансовых проблем заказчика, но проделана значительная работа.
Правильный ответ:
Ответ хороший — демонстрирует самостоятельность и способность вести проект end-to-end. Однако для позиции Golang-разработчика рекомендуется структурировать ответ по методике STAR (Situation, Task, Action, Result) и добавить технических деталей.
Как усилить ответ:
1. Контекст проекта (Situation). Опишите домен задачи: что за стартап, какая бизнес-проблема решалась, какие ограничения были (бюджет, сроки, команда).
2. Ваша роль и задачи (Task). Конкретизируйте: вы были единственным разработчиком? Была ли команда? Какие задачи вы решали — проектирование архитектуры, выбор стека, разработка, деплой?
3. Технические решения (Action). Это самая важная часть для технического интервью. Расскажите подробнее:
// Пример: если реализовывали высоконагруженный API
// Покажите понимание паттернов
type OrderService struct {
repo OrderRepository
cache *redis.Client
producer *kafka.Producer
validator *validator.Validate
}
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
// Валидация
if err := s.validator.Struct(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Идемпотентность через кэш
cacheKey := fmt.Sprintf("order:idempotency:%s", req.IdempotencyKey)
if exists, _ := s.cache.Exists(ctx, cacheKey).Result(); exists > 0 {
return nil, ErrDuplicateRequest
}
// Бизнес-логика
order := NewOrder(req)
if err := s.repo.Save(ctx, order); err != nil {
return nil, fmt.Errorf("failed to save order: %w", err)
}
// Асинхронная публикация события
event := OrderCreatedEvent{OrderID: order.ID, CreatedAt: order.CreatedAt}
go s.producer.Publish("orders.created", event)
// Кэширование результата
s.cache.Set(ctx, cacheKey, order.ID, 24*time.Hour)
return order, nil
}
4. Результат (Result). Приведите метрики: RPS системы, количество пользователей, снижение latency, улучшение стабильности. Даже если проект не был завершён, можно сказать о достигнутых промежуточных результатах.
5. Трудности и их преодоление. Расскажите о технических вызовах: проблемы с производительностью, race condition, утечки памяти, сложности с миграцией данных — и как вы их решали.
Пример усиленного ответа:
> «Горжусь проектом — платформа для обработки заказов в e-commerce стартапе. Был единственным бэкенд-разработчиком. Спроектировал микросервисную архитектуру на Go с использованием gRPC для inter-service communication. Реализовал паттерн Saga для распределённых транзакций, что позволило обеспечить консистентность данных при сбоях. Результат: система выдерживала 5000 RPS при latency P95 < 50ms. Проект не был запущен в production из-за закрытия стартапа, но полученный опыт проектирования отказоустойчивых систем считаю очень ценным.»
Вопрос 3. Что для вас важно при выборе нового работодателя?
Таймкод: 00:07:07:58
Ответ собеседника: Правильный. На первом месте — зарплатные ожидания. На втором месте — наличие команды, с которой можно обмениваться опытом и обсуждать проблемы, так как работа в одиночку замедляет развитие. Хотелось бы работать в команде над продуктом и совместно развиваться.
Правильный ответ:
Ответ честный и разумный. Желание работать в команде и профессионально расти — это важные мотиваторы. Для более полного ответа на позицию Golang-разработчика можно добавить несколько технических и процессных аспектов.
Как усилить ответ:
1. Технический стек и зрелость процессов. Для Golang-разработчика важно понимание технической культуры компании:
- Используется ли code review как стандартная практика?
- Есть ли линтеры и статический анализ (
golangci-lint,go vet)? - Как организован CI/CD pipeline?
- Используется ли мониторинг и observability (Prometheus, Grafana, Jaeger)?
2. Архитектурные практики. Вопросы, которые покажут вашу экспертизу:
- Какой подход к проектированию — монолит или микросервисы?
- Используется ли Domain-Driven Design?
- Как решаются вопросы распределённых транзакций и eventual consistency?
// Пример: если компания использует паттерн Outbox для надёжной публикации событий
// Это показывает зрелость архитектурных решений
type OutboxPublisher struct {
db *sql.DB
broker *kafka.Producer
}
func (p *OutboxPublisher) PublishEvent(ctx context.Context, event Event) error {
// Сохраняем событие в той же транзакции, что и бизнес-данные
return p.db.WithTx(ctx, func(tx *sql.Tx) error {
if err := p.saveEvent(tx, event); err != nil {
return err
}
return p.markAsPublished(tx, event.ID)
})
}
3. Кодовая база и технический долг. Стоит спросить о состоянии кодовой базы, наличии тестов, документации, подхода к рефакторингу.
4. Процессы разработки. Agile/Scrum/Kanban, длина спринтов, подход к планированию и оценке задач.
5. Рост и развитие. Возможности для профессионального роста: конференции, внутренние митапы, менторство, возможность влиять на технические решения.
Пример усиленного ответа:
> «Для меня важны несколько вещей. Во-первых, сильная инженерная культура — code review, тестирование, CI/CD. Во-вторых, возможность работать с современным стеком и влиять на технические решения. В-третьих, команда, в которой можно расти — обмениваться знаниями, обсуждать архитектурные решения. Также ценю прозрачность процессов и понимание бизнес-контекста задач.»
Вопрос 4. Что происходит, когда мы вводим URL в адресную строку браузера и нажимаем Enter?
Таймкод: 00:09:21
Ответ собеседника: Правильный. Браузер отправляет GET-запрос на сервер, сервер возвращает HTML-страницу. Затем происходит построение DOM-дерева, применяются стили, выполняется этап Painting (отрисовка), после чего подключаются JavaScript-файлы и навешиваются слушатели событий, что позволяет пользователю взаимодействовать со страницей. На более низком уровне запрос обрабатывается DNS-сервером, который находит соответствующий IP-адрес и направляет запрос по нужному адресу. Всё это работает на протоколах HTTP/HTTPS и TCP/IP.
Правильный ответ:
Ответ в целом правильный и покрывает основные этапы. Для позиции Golang-разработчика важно понимать полный цикл запроса, особенно серверную часть. Вот детальная разборка всех этапов.
1. Обработка ввода браузером. Браузер определяет, является ли введённый текст URL-адресом или поисковым запросом. Если это URL — проверяется протокол (http/https), при необходимости добавляется https://.
2. DNS-резолвинг. Браузер ищет IP-адрес сервера в следующем порядке:
- Кэш браузера
- Кэш ОС (
/etc/hosts, DNS cache) - DNS-резолвер провайдера
- Рекурсивный поиск: root DNS → TLD DNS → authoritative DNS
// Пример: простой DNS-резолвер на Go
package main
import (
"context"
"fmt"
"net"
"time"
)
func resolveDNS(domain string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resolver := &net.Resolver{
PreferGo: true, // Используем Go-реализацию вместо системной
}
addrs, err := resolver.LookupHost(ctx, domain)
if err != nil {
return nil, fmt.Errorf("DNS resolution failed: %w", err)
}
return addrs, nil
}
3. Установка TCP-соединения (Three-Way Handshake).
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK
4. TLS-рукопожатие (для HTTPS). Клиент и сервер согласовывают параметры шифрования:
- ClientHello (поддерживаемые версии TLS, cipher suites)
- ServerHello (выбранная версия, сертификат)
- Обмен ключами и установка защищённого соединения
// Пример: настройка TLS в Go-сервере
package main
import (
"crypto/tls"
"log"
"net/http"
)
func main() {
server := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, TLS!"))
}),
}
log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}
5. Отправка HTTP-запроса. Браузер формирует HTTP-запрос:
GET /path HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0...
Accept: text/html,application/xhtml+xml
Accept-Language: ru-RU,ru;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
6. Обработка запросом сервером. На стороне сервера (например, Go-приложение):
// Пример: обработка HTTP-запроса в Go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type Response struct {
Message string `json:"message"`
}
func handler(w http.ResponseWriter, r *http.Request) {
// Установка заголовков ответа
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", r.Header.Get("X-Request-ID"))
// Контекст с таймаутом
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Бизнес-логика
response := Response{Message: "Hello, World!"}
// Отправка ответа
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Failed to encode response: %v", err)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/hello", handler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
7. Получение и обработка ответа браузером. Браузер получает HTTP-ответ:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 26
Connection: keep-alive
Server: nginx/1.18.0
{"message":"Hello, World!"}
8. Парсинг HTML и построение DOM. Браузер парсит HTML и строит DOM-дерево. При обнаружении <link>, <script>, <img> — начинается загрузка ресурсов.
9. Построение CSSOM. Парсинг CSS-файлов и построение CSS Object Model.
10. Выполнение JavaScript. JS-движок выполняет скрипты, которые могут модифицировать DOM и CSSOM.
11. Построение Render Tree. Комбинация DOM и CSSOM формирует Render Tree.
12. Layout (Reflow). Вычисление размеров и позиций элементов.
13. Paint (Отрисовка). Запикселивание элементов.
14. Composite. Слои объединяются и отображаются на экране.
Дополнительные аспекты, которые стоит упомянуть:
- HTTP/2 и HTTP/3 — мультиплексирование, server push, QUIC
- Кэширование — Cache-Control, ETag, If-None-Match
- CDN — как контент доставляется ближе к пользователю
- Load Balancers — как запрос попадает на конкретный сервер
- Connection pooling — повторное использование TCP-соединений
Вопрос 5. Какие атрибуты загрузки скриптов вы знаете и чем отличаются async и defer?
Таймкод: 00:11:01
Ответ собеседника: Неполный. Основной атрибут — это src (путь до скрипта). Также есть async и defer, которые предназначены для асинхронной подгрузки скриптов (lazy loading). Оба похожи, но отличаются порядком загрузки: async загружает скрипты по мере их загрузки (без сохранения порядка), а defer сохраняет порядок загрузки скриптов, указанный в HTML.
Правильный ответ:
Ответ в целом правильный по разнице между async и defer, но неполный. Вот детальный разбор всех атрибутов и поведения скриптов.
Атрибуты тега script:
1. src — путь к внешнему файлу скрипта.
2. type — тип скрипта:
type="text/javascript"— стандартный JavaScript (по умолчанию)type="module"— ES-модуль, автоматически в strict mode, имеет свою область видимости
3. async — асинхронная загрузка без блокировки парсинга HTML.
4. defer — отложенное выполнение до завершения парсинга HTML.
5. nomodule — скрипт выполняется только в старых браузерах, не поддерживающих ES-модули.
6. crossorigin — управляет CORS-политикой для загрузки скриптов:
crossorigin="anonymous"— запрос без credentialscrossorigin="use-credentials"— запрос с cookies и auth
7. integrity — Subresource Integrity (SRI), проверка хеша содержимого скрипта.
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous"></script>
Сравнение режимов загрузки скриптов:
Обычный script (без атрибутов):
<script src="script.js"></script>
- Парсинг HTML приостанавливается
- Скрипт загружается и выполняется немедленно
- Парсинг HTML продолжается после выполнения
Script с async:
<script async src="script.js"></script>
- Загрузка скрипта параллельна парсингу HTML
- Скрипт выполняется сразу после загрузки (парсинг HTML приостанавливается)
- Порядок выполнения НЕ гарантируется
- Подходит для независимых скриптов: аналитика, реклама, счётчики
Script с defer:
<script defer src="script.js"></script>
- Загрузка скрипта параллельна парсингу HTML
- Скрипт выполняется ПОСЛЕ полного парсинга HTML (перед DOMContentLoaded)
- Порядок выполнения ГАРАНТИРОВАН (в порядке объявления в HTML)
- Подходит для скриптов, которым нужен полный DOM
Script module:
<script type="module" src="module.js"></script>
- Автоматически ведёт себя как defer
- Поддерживает import/export
- Автоматически в strict mode
Визуализация разницы:
Обычный script:
HTML: ████░░░░████████████
JS: ██████
Async:
HTML: ████████████████████
JS: ██████
Defer:
HTML: ████████████████████
JS: ████
Пример практического использования:
<!-- Аналитика — не зависит от DOM, порядок не важен -->
<script async src="https://analytics.example.com/tracker.js"></script>
<!-- Библиотеки — порядок важен -->
<script defer src="https://cdn.example.com/react.js"></script>
<script defer src="https://cdn.example.com/react-dom.js"></script>
<!-- Основной скрипт приложения -->
<script defer src="/js/app.js"></script>
<!-- ES-модуль (автоматически defer) -->
<script type="module" src="/js/main.mjs"></script>
Для Golang-разработчика это важно, потому что вы можете генерировать HTML-шаблоны на сервере и правильно расставлять скрипты:
// Пример: генерация HTML с правильными атрибутами скриптов в Go
package main
import (
"html/template"
"net/http"
)
type PageData struct {
Scripts []Script
}
type Script struct {
Src string
Async bool
Defer bool
Module bool
}
const tmpl = `
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
{{range .Scripts}}
<script src="{{.Src}}"
{{if .Async}}async{{end}}
{{if .Defer}}defer{{end}}
{{if .Module}}type="module"{{end}}>
</script>
{{end}}
</body>
</html>
`
func handler(w http.ResponseWriter, r *http.Request) {
data := PageData{
Scripts: []Script{
{Src: "/js/vendor.js", Defer: true},
{Src: "/js/app.js", Defer: true},
{Src: "https://analytics.example.com/tracker.js", Async: true},
},
}
t := template.Must(template.New("page").Parse(tmpl))
t.Execute(w, data)
}
Вопрос 6. Заблокирует ли большой CSS-файл размером 50 МБ рендеринг страницы во время загрузки?
Таймкод: 00:11:58
Ответ собеседника: Правильный. Да, скорее всего заблокирует. Поэтому рекомендуется разбивать большие файлы стилей на чанки (chunks) для оптимизации загрузки.
Правильный ответ:
Ответ правильный — CSS блокирует рендеринг. Вот более детальное объяснение почему и как с этим работать.
Почему CSS блокирует рендеринг:
CSS является render-blocking ресурсом, потому что браузер не может отобразить страницу без CSSOM (CSS Object Model). Браузер должен построить Render Tree, который является комбинацией DOM и CSSOM.
Важные нюансы:
1. CSS блокирует рендеринг, но НЕ блокирует парсинг HTML. HTML продолжает парситься, но отображения не происходит до загрузки CSS.
2. CSS НЕ блокирует выполнение JavaScript. Однако JavaScript блокирует рендеринг, если он зависит от CSSOM (например, обращение к стилям элементов).
3. Media queries позволяют сделать часть CSS неблокирующей:
<!-- Блокирует рендеринг только для экранов -->
<link rel="stylesheet" href="screen.css" media="screen">
<!-- НЕ блокирует рендеринг для печати -->
<link rel="stylesheet" href="print.css" media="print">
<!-- НЕ блокирует рендеринг для принтеров -->
<link rel="stylesheet" href="print.css" media="print">
Стратегии оптимизации загрузки CSS:
1. Critical CSS (критические стили). Инлайнить стили, необходимые для первого экрана:
<head>
<style>
/* Critical CSS — стили для above-the-fold контента */
body { margin: 0; font-family: sans-serif; }
.header { background: #333; color: white; }
.hero { height: 100vh; display: flex; }
</style>
<!-- Остальные стили загружаются асинхронно -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
2. Code Splitting. Разделение CSS на чанки по страницам/компонентам:
// Webpack — разделение CSS по точкам входа
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js',
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
};
3. CSS Modules / Scoped CSS. Изоляция стилей компонентов:
/* Button.module.css */
.button {
background: blue;
color: white;
}
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click me</button>;
}
4. Tree Shaking. Удаление неиспользуемого CSS с помощью PurgeCSS:
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.js'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
}),
],
};
5. Preload для критических ресурсов:
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
Для Golang-разработчика: если вы генерируете HTML на сервере, можно реализовать автоматическую инлайнацию critical CSS:
// Пример: серверная инлайнация critical CSS
package main
import (
"fmt"
"html/template"
"net/http"
)
type PageData struct {
CriticalCSS template.CSS
PageTitle string
}
func handler(w http.ResponseWriter, r *http.Request) {
// В реальности — чтение из файла или генерация
criticalCSS := template.CSS(`
body { margin: 0; font-family: sans-serif; }
.header { background: #333; color: white; }
`)
data := PageData{
CriticalCSS: criticalCSS,
PageTitle: "My Page",
}
tmpl := `
<!DOCTYPE html>
<html>
<head>
<title>{{.PageTitle}}</title>
<style>{{.CriticalCSS}}</style>
<link rel="preload" href="/static/styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
<div class="header">Header</div>
</body>
</html>
`
t := template.Must(template.New("page").Parse(tmpl))
t.Execute(w, data)
}
Итого: CSS блокирует рендеринг, но не парсинг HTML. Для оптимизации используйте critical CSS, code splitting, preload и media queries.
Вопрос 7. Работали ли вы с веб-сокетами и другими протоколами? Что можете рассказать о GraphQL?
Таймкод: 00:12:36
Ответ собеседника: Правильный. Да, был опыт работы с веб-сокетами и графикой. GraphQL — это удобная технология, главным плюсом которой является единая строгая схема типов между бэкендом и фронтендом, которую можно автоматически генерировать скриптом и поддерживать в актуальном состоянии.
Правильный ответ:
Ответ правильный, но довольно поверхностный. Для Golang-позиции важно показать глубокое понимание протоколов и практический опыт. Вот детальный разбор.
WebSockets:
WebSocket — это полнодуплексный протокол поверх TCP, обеспечивающий постоянное соединение между клиентом и сервером.
Как работает WebSocket:
1. HTTP Handshake:
Client → Server: GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZQ==
Server → Client: HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. Двусторонняя передача данных через фреймы
Реализация WebSocket на Go:
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // В продакшене — проверка origin
},
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mu sync.RWMutex
}
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
type Message struct {
Type string `json:"type"`
Payload string `json:"payload"`
Room string `json:"room,omitempty"`
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("Client connected. Total: %d", len(h.clients))
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
log.Printf("Client disconnected. Total: %d", len(h.clients))
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(512 * 1024) // 512KB лимит
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
break
}
// Парсинг и валидация сообщения
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
log.Printf("Invalid message format: %v", err)
continue
}
c.hub.broadcast <- message
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Ожидающие сообщения отправляем вместе
n := len(c.send)
for i := 0; i < n; i++ {
w.Write([]byte{'\n'})
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
client := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, 256),
}
client.hub.register <- client
go client.writePump()
go client.readPump()
}
func main() {
hub := newHub()
go hub.run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
GraphQL:
GraphQL — это язык запросов для API, разработанный Facebook, позволяющий клиенту запрашивать именно те данные, которые ему нужны.
Преимущества GraphQL:
- Типобезопасность — строгая схема определяет все типы данных
- Нет over-fetching и under-fetching — клиент получает ровно то, что запросил
- Единая точка входа — один endpoint вместо множества REST-ресурсов
- Интроспекция — возможность запросить схему API
Недостатки GraphQL:
- Сложность кэширования — POST-запросы сложнее кэшировать
- N+1 проблема — без оптимизации запросы могут быть неэффективными
- Сложность rate limiting — сложно ограничить сложность запросов
Реализация GraphQL на Go:
// Пример GraphQL-сервера на Go с использованием gqlgen
package main
import (
"context"
"database/sql"
"log"
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/vektah/gqlparser/v2/ast"
)
// Схема GraphQL
const schema = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`
// Резолверы
type Resolver struct {
db *sql.DB
}
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
type queryResolver struct{ *Resolver }
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
// DataLoader для решения N+1 проблемы
user, err := loadUser(ctx, id)
if err != nil {
return nil, err
}
return user, nil
}
func (r *queryResolver) Users(ctx context.Context) ([]*User, error) {
// Оптимизированный запрос с JOIN
rows, err := r.db.QueryContext(ctx, `
SELECT u.id, u.name, u.email
FROM users u
ORDER BY u.created_at DESC
LIMIT 100
`)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, err
}
users = append(users, &u)
}
return users, nil
}
// DataLoader для решения N+1 проблем
type UserLoader struct {
db *sql.DB
}
func (l *UserLoader) BatchGetUsers(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
userIDs := make([]string, len(keys))
for i, key := range keys {
userIDs[i] = key.String()
}
// Один запрос вместо N
query := `
SELECT id, name, email
FROM users
WHERE id = ANY($1)
`
rows, err := l.db.QueryContext(ctx, query, pq.Array(userIDs))
if err != nil {
return []*dataloader.Result{{Error: err}}
}
defer rows.Close()
userMap := make(map[string]*User)
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
continue
}
userMap[u.ID] = &u
}
results := make([]*dataloader.Result, len(keys))
for i, key := range keys {
if user, ok := userMap[key.String()]; ok {
results[i] = &dataloader.Result{Data: user}
} else {
results[i] = &dataloader.Result{Error: errors.New("user not found")}
}
}
return results
}
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
resolver := &Resolver{db: db}
srv := handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))
// Middleware для контекста
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
// Добавляем DataLoader в контекст
ctx = context.WithValue(ctx, userLoaderKey, &UserLoader{db: db})
return next(ctx)
})
http.Handle("/", playground.Handler("GraphQL", "/query"))
http.Handle("/query", srv)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Другие протоколы, которые стоит знать:
1. gRPC — бинарный протокол на основе Protocol Buffers:
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
string id = 1;
}
2. Server-Sent Events (SSE) — односторонняя передача от сервера к клиенту:
func sseHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339))
flusher.Flush()
case <-r.Context().Done():
return
}
}
}
3. MQTT — лёгкий протокол для IoT и real-time коммуникации.
4. HTTP/2 Server Push — сервер может отправлять ресурсы до того, как клиент их запросит.
Вопрос 8. Что такое CORS (Cross-Origin Resource Sharing)?
Таймкод: 00:13:12
Ответ собеседника: Правильный. CORS — это политика браузера, связанная с Same-Origin Policy. Когда ресурсы запрашиваются с одного источника (одного домена), браузер считает это безопасным. При попытке запросить ресурсы с другого источника браузер блокирует такой доступ как небезопасный. CORS позволяет настраивать доступ между различными источниками через специальные HTTP-заголовки, определяя какие домены, методы (GET, POST, PUT) и заголовки разрешены или запрещены.
Правильный ответ:
Ответ правильный и покрывает основы. Для Golang-разработчика важно также понимать технические детали реализации. Вот более полный разбор.
Same-Origin Policy (SOP):
Два URL имеют одинаковый origin, если совпадают три компонента:
- Протокол (http/https)
- Хост (domain)
- Порт
https://example.com:443/page1 и https://example.com:443/page2 — одинаковый origin
https://example.com:443/page1 и http://example.com:443/page2 — разный origin (протокол)
https://example.com:443/page1 и https://api.example.com:443/page2 — разный origin (хост)
https://example.com:443/page1 и https://example.com:8080/page2 — разный origin (порт)
Как работает CORS:
1. Simple Requests (простые запросы). Для простых запросов браузер сразу отправляет запрос с заголовком Origin:
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Сервер отвечает с заголовком Access-Control-Allow-Origin:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
2. Preflight Requests (предварительные запросы). Для «непростых» запросов браузер сначала отправляет OPTIONS-запрос:
Условия для preflight:
- Методы кроме GET, HEAD, POST
- Кастомные заголовки (например, Authorization)
- Content-Type кроме
application/x-www-form-urlencoded,multipart/form-data,text/plain
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type
Сервер отвечает:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
CORS-заголовки:
Access-Control-Allow-Origin— разрешённые origin (*или конкретный домен)Access-Control-Allow-Methods— разрешённые HTTP-методыAccess-Control-Allow-Headers— разрешённые заголовкиAccess-Control-Allow-Credentials— разрешены ли cookies и авторизацияAccess-Control-Max-Age— время кэширования preflight-ответаAccess-Control-Expose-Headers— заголовки, доступные клиенту
Реализация CORS в Go:
package main
import (
"log"
"net/http"
"strings"
"time"
)
// CORS middleware
func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Проверка разрешённых origins
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
// Не используйте * с credentials: true
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
w.Header().Set("Access-Control-Expose-Headers", "X-Request-ID, X-Total-Count")
}
// Preflight request
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// С использованием библиотеки rs/cors
func withLibraryCORS() http.Handler {
cors := cors.New(cors.Options{
AllowedOrigins: []string{"https://frontend.example.com", "https://admin.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type", "X-Request-ID"},
AllowCredentials: true,
MaxAge: 86400,
Debug: false,
})
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
return cors.Handler(mux)
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"users": []}`))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
// Применяем CORS middleware
handler := corsMiddleware([]string{
"https://frontend.example.com",
"https://admin.example.com",
})(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
Частые проблемы и решения:
1. Credentials с wildcard. Нельзя использовать Access-Control-Allow-Origin: * вместе с Access-Control-Allow-Credentials: true. Браузер заблокирует такой запрос.
2. Кэширование preflight. Используйте Access-Control-Max-Age для кэширования preflight-ответа и уменьшения количества запросов.
3. Vary: Origin. При динамическом определении origin добавляйте заголовок Vary: Origin для правильного кэширования:
w.Header().Set("Vary", "Origin")
4. Безопасность. Никогда не отражайте Origin без валидации — это может привести к уязвимостям:
// ПЛОХО — отражение Origin без проверки
origin := r.Header.Get("Origin")
w.Header().Set("Access-Control-Allow-Origin", origin)
// ХОРОШО — проверка белого списка
if isAllowedOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
Вопрос 9. Как реализовать авторизацию с использованием токенов (JWT)?
Таймкод: 00:14:49
Ответ собеседника: Правильный. Пользователь отправляет логин и пароль на бэкенд. Бэкенд генерирует пару токенов: Access и Refresh. Токены сохраняются в куках (чаще всего HttpOnly для защиты от изменения с фронта). Refresh-токен долгоживущий, Access-токен короткоживущий. С каждым запросом фронтенд подставляет Access-токен, и бэкенд распознаёт пользователя. Когда Access-токен умирает, бэкенд использует Refresh-токен для обновления пары токенов.
Правильный ответ:
Ответ правильный и покрывает основной поток. Для Golang-разработчика важно показать понимание безопасности и деталей реализации. Вот полный разбор.
Структура JWT:
JWT состоит из трёх частей, разделённых точками:
header.payload.signature
Header — алгоритм и тип токена:
{
"alg": "RS256",
"typ": "JWT"
}
Payload — данные пользователя (claims):
{
"sub": "user-uuid-123",
"name": "John Doe",
"role": "admin",
"iat": 1699999999,
"exp": 1700003599,
"jti": "unique-token-id-456"
}
Signature — подпись для проверки целостности:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Алгоритмы подписи:
- HS256/HS384/HS512 — HMAC с симметричным ключом (один секрет)
- RS256/RS384/RS512 — RSA с асимметричными ключами (публичный/приватный)
- ES256/ES384/ES512 — ECDSA с эллиптическими кривыми
Рекомендация: Используйте RS256 для микросервисной архитектуры — сервисы могут проверять токен публичным ключом, не зная приватного.
Полная реализация на Go:
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
// Типы и константы
const (
AccessTokenTTL = 15 * time.Minute
RefreshTokenTTL = 7 * 24 * time.Hour
)
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
}
type Claims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
JTI string `json:"jti"` // JWT ID для отзыва токена
jwt.RegisteredClaims
}
type User struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
Role string `json:"role"`
}
// Сервис аутентификации
type AuthService struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
refreshStore RefreshTokenStore // Интерфейс для хранения refresh-токенов
}
type RefreshTokenStore interface {
Save(ctx context.Context, userID string, tokenID string, ttl time.Duration) error
Validate(ctx context.Context, userID string, tokenID string) (bool, error)
Revoke(ctx context.Context, userID string, tokenID string) error
RevokeAll(ctx context.Context, userID string) error
}
func NewAuthService(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, store RefreshTokenStore) *AuthService {
return &AuthService{
privateKey: privateKey,
publicKey: publicKey,
refreshStore: store,
}
}
// Генерация пары токенов
func (s *AuthService) GenerateTokenPair(ctx context.Context, user *User) (*TokenPair, error) {
now := time.Now()
accessJTI := uuid.New().String()
// Access Token
accessClaims := Claims{
UserID: user.ID,
Role: user.Role,
JTI: accessJTI,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(AccessTokenTTL)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "my-service",
Subject: user.ID,
ID: accessJTI,
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodRS256, accessClaims)
accessTokenString, err := accessToken.SignedString(s.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign access token: %w", err)
}
// Refresh Token
refreshJTI := uuid.New().String()
refreshClaims := jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(RefreshTokenTTL)),
IssuedAt: jwt.NewNumericDate(now),
Issuer: "my-service",
Subject: user.ID,
ID: refreshJTI,
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodRS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(s.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign refresh token: %w", err)
}
// Сохраняем refresh-токен в хранилище
if err := s.refreshStore.Save(ctx, user.ID, refreshJTI, RefreshTokenTTL); err != nil {
return nil, fmt.Errorf("failed to save refresh token: %w", err)
}
return &TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresAt: now.Add(AccessTokenTTL),
}, nil
}
// Валидация Access Token
func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Проверка метода подписи
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token claims")
}
return claims, nil
}
// Обновление пары токенов
func (s *AuthService) RefreshTokens(ctx context.Context, refreshTokenString string) (*TokenPair, error) {
// Парсим refresh-токен
token, err := jwt.ParseWithClaims(refreshTokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !ok || !token.Valid {
return nil, errors.New("invalid refresh token claims")
}
// Проверяем, что refresh-токен не отозван
valid, err := s.refreshStore.Validate(ctx, claims.Subject, claims.ID)
if err != nil || !valid {
return nil, errors.New("refresh token has been revoked")
}
// Отзываем старый refresh-токен (rotation)
if err := s.refreshStore.Revoke(ctx, claims.Subject, claims.ID); err != nil {
return nil, fmt.Errorf("failed to revoke old refresh token: %w", err)
}
// Генерируем новую пару токенов
user := &User{
ID: claims.Subject,
Role: "user", // В реальности — загрузка из БД
}
return s.GenerateTokenPair(ctx, user)
}
// Логин
func (s *AuthService) Login(ctx context.Context, email, password string) (*TokenPair, error) {
// В реальности — загрузка из БД
user, err := s.loadUserByEmail(ctx, email)
if err != nil {
return nil, errors.New("invalid credentials")
}
// Проверка пароля
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, errors.New("invalid credentials")
}
return s.GenerateTokenPair(ctx, user)
}
// Выход — отзыв всех токенов
func (s *AuthService) Logout(ctx context.Context, userID string) error {
return s.refreshStore.RevokeAll(ctx, userID)
}
func (s *AuthService) loadUserByEmail(ctx context.Context, email string) (*User, error) {
// Заглушка — в реальности запрос к БД
return &User{
ID: "user-123",
Email: email,
PasswordHash: "$2a$10$...", // bcrypt hash
Role: "user",
}, nil
}
// Middleware для аутентификации
func authMiddleware(authService *AuthService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
claims, err := authService.ValidateAccessToken(parts[1])
if err != nil {
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
return
}
// Добавляем claims в контекст
ctx := context.WithValue(r.Context(), "userClaims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Middleware для авторизации по ролям
func roleMiddleware(allowedRoles ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value("userClaims").(*Claims)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
for _, role := range allowedRoles {
if claims.Role == role {
next.ServeHTTP(w, r)
return
}
}
http.Error(w, "Forbidden", http.StatusForbidden)
})
}
}
// HTTP Handlers
type Handler struct {
authService *AuthService
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
tokenPair, err := h.authService.Login(r.Context(), req.Email, req.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// Устанавливаем refresh-токен в HttpOnly cookie
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: tokenPair.RefreshToken,
HttpOnly: true,
Secure: true, // Только через HTTPS
SameSite: http.SameSiteStrictMode,
Path: "/api/auth/refresh",
MaxAge: int(RefreshTokenTTL.Seconds()),
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tokenPair)
}
func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("refresh_token")
if err != nil {
http.Error(w, "Refresh token required", http.StatusUnauthorized)
return
}
tokenPair, err := h.authService.RefreshTokens(r.Context(), cookie.Value)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: tokenPair.RefreshToken,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/api/auth/refresh",
MaxAge: int(RefreshTokenTTL.Seconds()),
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tokenPair)
}
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("userClaims").(*Claims)
if err := h.authService.Logout(r.Context(), claims.UserID); err != nil {
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
// Очищаем cookie
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: "",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/api/auth/refresh",
MaxAge: -1,
})
w.WriteHeader(http.StatusNoContent)
}
func main() {
// Генерация RSA-ключей (в продакшене — загрузка из файла)
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
authService := NewAuthService(privateKey, publicKey, nil)
handler := &Handler{authService: authService}
mux := http.NewServeMux()
// Публичные маршруты
mux.HandleFunc("/api/auth/login", handler.Login)
mux.HandleFunc("/api/auth/refresh", handler.Refresh)
// Защищённые маршруты
authHandler := authMiddleware(authService)(mux)
// Админские маршруты
adminHandler := roleMiddleware("admin")(mux)
server := &http.Server{
Addr: ":8080",
Handler: authHandler,
}
server.ListenAndServe()
}
Важные аспекты безопасности:
1. Refresh Token Rotation. При каждом обновлении refresh-токена старый отзывается. Если украденный refresh-токен используется повторно — все токены пользователя отзываются.
2. Token Binding. Привязка токена к устройству/IP для защиты от кражи.
3. Blacklist для access-токенов. Для критичных операций (выход, смена пароля) — проверка в Redis.
4. Короткий срок жизни access-токена. Обычно 5-15 минут — минимизирует ущерб при компрометации.
5. Secure cookie атрибуты:
HttpOnly— защита от XSSSecure— только HTTPSSameSite=Strict— защита от CSRF
6. Не храните чувствительные данные в JWT payload. Токен можно декодировать (base64), подпись защищает только от подделки.
Вопрос 10. Как вы предпочитаете работать со стилями? Какие препроцессоры и CSS-библиотеки используете?
Таймкод: 00:16:15
Ответ собеседования: Правильный. Последнее время предпочитаю писать на нативном CSS, так как препроцессоры замедляют сборку. На больших проектах отказ от препроцессоров может значительно ускорить сборку. Предпочитаю разработку компонентов по дизайн-системе, согласованной с дизайнером. Интерфейс строится из готовых компонентов-кирпичиков с заданными отступами, размерами и вариантами. CSS-библиотеки (типа styled-components) не очень нравятся из-за потери производительности при рендере (на 100 тысячах элементов разница может быть 10-20%). Рассматриваю Zero-runtime библиотеки стилей будущего, которые компилируются в CSS на этапе сборки.
Правильный ответ:
Ответ правильный и демосонстрирует зрелый подход к стилизации. Для Golang-разработчика это вопрос скорее о понимании фронтенд-экосистемы при full-stack работе. Вот дополнительные детали.
Современные подходы к стилизации:
1. CSS Modules. Локальная область видимости стилей без runtime-оверхеда:
/* Button.module.css */
.button {
padding: 8px 16px;
border-radius: 4px;
border: none;
}
.primary {
background: #007bff;
color: white;
}
.secondary {
background: #6c757d;
color: white;
}
import styles from './Button.module.css';
function Button({ variant, children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
2. Utility-first CSS (Tailwind CSS). Zero-runtime подход — только используемые классы попадают в бандл:
function Button({ variant, children }) {
const baseClasses = "px-4 py-2 rounded border-none font-medium";
const variantClasses = {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-500 text-white hover:bg-gray-600",
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`}>
{children}
</button>
);
}
3. CSS-in-JS Zero-runtime (vanilla-extract, Linaria). Стили извлекаются в отдельные CSS-файлы на этапе сборки:
// vanilla-extract
import { style } from '@vanilla-extract/css';
export const button = style({
padding: '8px 16px',
borderRadius: '4px',
border: 'none',
});
export const primary = style({
backgroundColor: '#007bff',
color: 'white',
});
CSS-препроцессоры:
SASS/SCSS — наиболее распространённый:
$primary-color: #007bff;
$spacing-unit: 8px;
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.button {
padding: ($spacing-unit) ($spacing-unit * 2);
background: $primary-color;
&--primary {
background: $primary-color;
}
&:hover {
background: darken($primary-color, 10%);
}
}
PostCSS — трансформация CSS с плагинами (autoprefixer, cssnano, custom properties).
Современные CSS-фичи, заменяющие препроцессоры:
- CSS Custom Properties — переменные без препроцессора
- CSS Nesting — вложенность нативно
- CSS Container Queries — адаптивность по контейнеру
- CSS Layers — управление специфичностью
Для Golang-разработчика: если вы генерируете HTML на сервере, можно интегрировать CSS-сборку в pipeline:
// Пример: интеграция Tailwind CSS в Go-проект
package main
import (
"html/template"
"net/http"
)
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/tailwind.min.css">
</head>
<body>
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-blue-600">{{.Title}}</h1>
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
{{.ButtonText}}
</button>
</div>
</body>
</html>
`
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.New("page").Parse(htmlTemplate))
data := struct {
Title string
ButtonText string
}{
Title: "Hello from Go",
ButtonText: "Click me",
}
tmpl.Execute(w, data)
}
Рекомендация: Для больших проектов — CSS Modules или Tailwind CSS. Для компонентных библиотек — vanilla-extlect. Избегайте runtime CSS-in-JS (styled-components, emotion) в высоконагруженных приложениях.
Вопрос 11. Что такое специфичность CSS-селекторов? Как она рассчитывается?
Таймкод: 00:18:49
Ответ собеседника: Правильный. Каждый CSS-селектор обладает своей специфичностью. У идентификатора (ID) она она выше, чем у класса. У класса выше, чем у названия элемента. Специфичность имеет таблицу значений: по названию блока — 1, по классу — 10, по ID — 100, инлайн-стили — 1000, !important — 10000. Чем выше специфичность, тем более приоритетный стиль может перезаписать стиль с меньшей специфичностью.
Правильный ответ:
Ответ правильный по сути, но упрощает систему расчёта. Вот более точное объяснение.
Специфичность CSS — это алгоритм определения приоритета стилей при конфликте правил для одного элемента.
Формула расчёта (A-B-C-D):
Специфичность представляется как четвёрка значений:
| Уровень | Значение | Примеры |
|---|---|---|
| A | Инлайн-стили | style="color: red" |
| B | ID-селекторы | #header, #nav |
| C | Классы, атрибуты, псевдоклассы | .button, [type="text"], :hover |
| D | Элементы, псевдоэлементы | div, ::before, ::after |
Примеры расчёта:
/* Специфичность: 0-0-0-1 (D=1) */
div {
color: black;
}
/* Специфичность: 0-0-1-0 (C=1) */
.button {
color: blue;
}
/* Специфичность: 0-0-1-1 (C=1, D=1) */
button.button {
color: green;
}
/* Специфичность: 0-0-2-0 (C=2) */
.button.primary {
color: red;
}
/* Специфичность: 0-1-0-0 (B=1) */
#header {
color: purple;
}
/* Специфичность: 0-1-1-1 (B=1, C=1, D=1) */
header#header.nav {
color: orange;
}
/* Специфичность: 0-1-2-0 (B=1, C=2) */
#header .nav.active {
color: pink;
}
/* Специфичность: 1-0-0-0 (A=1) */
/* style="color: white" в HTML */
Правила сравнения:
Сравнение идёт слева направо: A → B → C → D. Как только найдена разница — сравнение прекращается.
0-1-0-0 > 0-0-10-10
0-0-2-0 > 0-0-1-10
0-0-1-0 > 0-0-0-100
Важные нюансы:
1. Наследование не имеет специфичности. Наследуемые стили проигрывают любым явно указанным:
/* Специфичность: 0-0-1-0 */
.parent {
color: red;
}
/* Специфичность: 0-0-0-1 */
span {
color: blue; /* Победит, даже если span внутри .parent */
}
2. Универсальный селектор (*) имеет специфичность 0-0-0-0:
/* Специфичность: 0-0-0-0 */
* {
color: black;
}
/* Специфичность: 0-0-0-1 — победит */
div {
color: blue;
}
3. :not() и :is() — специфичность самого специфичного аргумента:
/* Специфичность: 0-0-1-0 (от .button) */
:not(.button) {
color: red;
}
/* Специфичность: 0-1-0-0 (от #header) */
:is(#header, .nav) {
color: blue;
}
4. !important переопределяет всё:
/* Победит даже инлайн-стили */
.button {
color: red !important;
}
5. Одинаковая специфичность — побеждает последний объявленный:
.button { color: red; }
.button { color: blue; } /* Победит */
Проблемы с высокой специфичностью:
/* Антипаттерн — слишком специфичный селектор */
div#main.container > ul.nav li.active a.link {
color: red;
}
/* Проблема: чтобы переопределить, нужен ещё более специфичный селектор */
/* Это приводит к "войне специфичности" */
Рекомендации:
- Избегайте ID в CSS-селекторах
- Используйте классы как основной способ стилизации
- Ограничивайте вложенность 2-3 уровнями
- Не используйте !important (кроме utility-классов)
- Рассмотрите BEM-методологию для предсказуемой специфичности
/* BEM — плоская структура с предсказуемой специфичностью */
.block { }
.block__element { }
.block--modifier { }
.block__element--modifier { }
CSS Layers (@layer) — современный способ управления приоритетом:
@layer reset, base, components, utilities;
@layer base {
.button { color: blue; } /* Специфичность: 0-0-1-0 */
}
@layer utilities {
.text-red { color: red; } /* Проигрывает base, несмотря на ту же специфичность */
}
Вопрос 12. Какое у вас отношение к !important в CSS?
Таймкод: 00:19:34
Ответ собеседника: Правильный. Негативное. Бывают проекты, где из-за частого использования !important и инлайн-стилей образуется клубок, который трудно распутать. Верстка начинает ехать, сложно найти где что перезатирает. Лучше не использовать !important за редкими исключениями.
Правильный ответ:
Ответ правильный и демонстрирует зрелый подход. Вот дополнительные детали о случаях, когда !important допустим, и альтернативы.
Почему !important — это проблема:
1. Нарушает каскад. CSS расшифровывается как Cascading Style Sheets — каскад является фундаментальной особенностью. !important ломает этот механизм.
2. Создаёт «войну специфичности». Один !important требует другой для переопределения:
.button { color: blue !important; }
/* Чтобы переопределить: */
.button { color: red !important; } /* Антипаттерн */
3. Усложняет отладку. Сложно понять, почему стиль не применяется.
4. Невозможно переопределить из JavaScript. element.style.color = 'green' не переопределит !important.
Допустимые случаи использования !important:
1. Utility-классы (Tailwind CSS подход):
/* Эти классы всегда должны применяться */
.hidden {
display: none !important;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
}
2. Переопределение стилей сторонних библиотек:
/* Библиотека использует инлайн-стили или очень специфичные селекторы */
.third-party-widget .container {
padding: 0 !important;
}
3. Accessibility — стили для принудительного контраста:
/* Режим высокого контраста */
@media (prefers-contrast: high) {
* {
background: white !important;
color: black !important;
}
}
4. Print-стили:
@media print {
.no-print {
display: none !important;
}
}
Альтернативы !important:
1. Повышение специфичности селектора:
/* Вместо !important */
.button { color: blue !important; }
/* Используйте более специфичный селектор */
.button.button { color: blue; } /* 0-0-2-0 */
/* или */
.parent .button { color: blue; } /* 0-0-2-0 */
2. CSS Custom Properties (переменные):
:root {
--button-color: blue;
}
.button {
color: var(--button-color);
}
/* Переопределение без !important */
.special-button {
--button-color: red;
}
3. @layer (CSS Layers):
@layer components, overrides;
@layer components {
.button { color: blue; }
}
@layer overrides {
.button { color: red; } /* Победит из-за порядка слоёв */
}
4. CSS Modules / Scoped Styles. Автоматическая изоляция стилей предотвращает конфликты без !important.
/* Button.module.css */
.button {
color: blue; /* Не конфликтует с другими .button */
}
5. BEM-методология. Плоская структура селекторов с предсказуемой специфичностью:
/* Нет вложенности — нет проблем со специфичностью */
.block { }
.block__element { }
.block--modifier { }
Рекомендация: Считайте !important запахом кода (code smell). Если вы его используете — это сигнал, что архитектура стилей требует рефакторинга.
Вопрос 13. Расскажите о механизме Event Loop в JavaScript.
Таймкод: 00:20:04
Ответ собеседника: Правильный. JavaScript — однопоточный язык с механизмом асинхронного выполнения. Есть стек вызовов, макрозадачи (setTimeout, события) и микрозадачи (промисы). Микрозадачи имеют более высокий приоритет.
Правильный ответ:
Ответ правильный и покрывает основы. Для Golang-разработчика важно понимание асинхронных паттернов, так как они концептуально похожи на горутины и каналы в Go. Вот детальный разбор.
Компоненты Event Loop:
1. Call Stack (стек вызовов). LIFO-структура для выполнения синхронного кода:
function first() {
console.log('first');
second();
}
function second() {
console.log('second');
third();
}
function third() {
console.log('third');
}
first();
// Call Stack:
// 1. first() добавляется
// 2. console.log('first') → выполняется → убирается
// 3. second() добавляется
// 4. console.log('second') → выполняется → убирается
// 5. third() добавляется
// 6. console.log('third') → выполняется → убирается
// 7. third() убирается
// 8. second() убирается
// 9. first() убирается
2. Web APIs. Браузерные API (setTimeout, fetch, DOM events), которые выполняют операции вне основного потока.
3. Task Queue (Macrotask Queue). Очередь макрозадач:
setTimeout,setInterval- DOM events (click, keypress)
setImmediate(Node.js)requestAnimationFrame(браузер)
4. Microtask Queue. Очередь микрозадач:
Promise.then/catch/finallyqueueMicrotask()MutationObserverprocess.nextTick(Node.js, высший приоритет)
Алгоритм Event Loop:
1. Выполнить весь синхронный код в Call Stack
2. Когда Call Stack пуст:
a. Выполнить ВСЕ микрозадачи (до опустошения очереди)
b. Выполнить ОДНУ макрозадачу
c. Повторить с шага 2
Пример порядка выполнения:
console.log('1 - Sync');
setTimeout(() => {
console.log('2 - Macrotask (setTimeout 0)');
}, 0);
Promise.resolve().then(() => {
console.log('3 - Microtask (Promise)');
});
queueMicrotask(() => {
console.log('4 - Microtask (queueMicrotask)');
});
Promise.resolve()
.then(() => {
console.log('5 - Microtask (Promise chain 1)');
})
.then(() => {
console.log('6 - Microtask (Promise chain 2)');
});
console.log('7 - Sync');
// Вывод:
// 1 - Sync
// 7 - Sync
// 3 - Microtask (Promise)
// 4 - Microtask (queueMicrotask)
// 5 - Microtask (Promise chain 1)
// 6 - Microtask (Promise chain 2)
// 2 - Macrotask (setTimeout 0)
Более сложный пример:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
// Вывод:
// script start
// script end
// promise1
// promise2
// setTimeout
Вложенные микрозадачи:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve()
.then(() => {
console.log('promise1');
// Новая микрозадача добавляется в очередь
// и выполнится ДЕСЛЕДУЮЩЕЙ макрозадачи
Promise.resolve().then(() => {
console.log('promise1-1');
});
})
.then(() => {
console.log('promise2');
});
console.log('end');
// Вывод:
// start
// end
// promise1
// promise2
// promise1-1
// timeout
Аналогия с Go:
Для Golang-разработчика Event Loop можно сравнить с горутинами и каналами:
// Аналогия Event Loop в Go
package main
import (
"fmt"
"time"
)
func main() {
// Call Stack — синхронный код
fmt.Println("1 - Sync")
// Macrotask — аналог setTimeout
time.AfterFunc(0, func() {
fmt.Println("2 - Macrotask (setTimeout)")
})
// Microtask — аналог Promise (выполняется сразу)
go func() {
fmt.Println("3 - Microtask (Promise)")
}()
// Даём горутинам время выполниться
time.Sleep(100 * time.Millisecond)
fmt.Println("4 - Sync")
}
Ключевые отличия:
- В Go горутины выполняются параллельно (на нескольких потоках)
- В JavaScript — однопоточный Event Loop
- Каналы в Go — синхронизация; в JavaScript — нет аналога
Частые ошибки:
// Блокировка Event Loop
function heavyTask() {
for (let i = 0; i < 1e9; i++) {
// Блокирует основной поток
}
}
// Решение: разбить на части с помощью setTimeout или Web Workers
function chunkedTask(items, chunkSize = 1000) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, items.length);
for (; index < end; index++) {
// Обработка items[index]
}
if (index < items.length) {
setTimeout(processChunk, 0); // Отдаём управление Event Loop
}
}
processChunk();
}
requestAnimationFrame — специальный случай:
// Выполняется перед отрисовкой (обычно 60fps)
requestAnimationFrame(() => {
console.log('Before render');
});
// Порядок:
// 1. Синхронный код
// 2. Микрозадачи
// 3. requestAnimationFrame
// 4. Отрисовка (render)
// 5. Макрозадачи
Вопрос 14. Напишите реализацию функции debounce.
Таймкод: 00:24:05
Ответ собеседника: Правильный. Функция debounce была успешно реализована. Код корректно откладывает выполнение функции и сбрасывает таймер при повторных вызовах. При вызове debounce с разными аргументами пять раз подряд, в консоль выводится только последний результат (5).
Правильный ответ:
Ответ правильный. Вот полная реализация debounce с различными опциями и примерами использования.
Базовая реализация debounce:
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Очищаем предыдущий таймер
clearTimeout(timeoutId);
// Устанавливаем новый таймер
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Использование
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// API call
}, 300);
// Вызов при вводе в поле поиска
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Продвинутая реализация с опциями:
function debounce(func, delay, options = {}) {
let timeoutId;
let lastCallTime;
let lastInvokeTime = 0;
let lastArgs = null;
let lastThis = null;
let result;
const { leading = false, trailing = true, maxWait } = options;
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = null;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc, wait) {
return setTimeout(pendingFunc, wait);
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = delay - timeSinceLastCall;
return maxWait !== undefined
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined ||
timeSinceLastCall >= delay ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
);
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Перезапускаем таймер
timeoutId = startTimer(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timeoutId = undefined;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = null;
return result;
}
function leadingEdge(time) {
lastInvokeTime = time;
timeoutId = startTimer(timerExpired, delay);
return leading ? invokeFunc(time) : result;
}
function cancel() {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = undefined;
}
function flush() {
if (timeoutId === undefined) {
return result;
}
return trailingEdge(Date.now());
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxWait !== undefined) {
timeoutId = startTimer(timerExpired, delay);
return invokeFunc(lastCallTime);
}
}
if (timeoutId === undefined) {
timeoutId = startTimer(timerExpired, delay);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
Примеры использования:
// 1. Базовый debounce для поиска
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(async (query) => {
const results = await fetch(`/api/search?q=${query}`);
const data = await results.json();
renderResults(data);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 2. Debounce с leading edge (немедленный первый вызов)
const debouncedClick = debounce(() => {
console.log('Button clicked!');
}, 1000, { leading: true, trailing: false });
// Первый клик — немедленно, последующие игнорируются 1 секунду
// 3. Debounce с maxWait (максимальное время ожидания)
const debouncedSave = debounce((data) => {
console.log('Saving:', data);
}, 300, { maxWait: 1000 });
// Сохранится максимум через 1 секунду, даже если продолжаем вводить
// 4. Отмена и принудительный вызов
const debouncedUpdate = debounce(updateUI, 500);
// Отменить отложенный вызов
debouncedUpdate.cancel();
// Принудительно вызвать немедленно
debouncedUpdate.flush();
Debounce в React:
import { useState, useEffect, useCallback, useRef } from 'react';
// Хук useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Хук useDebouncedCallback
function useDebouncedCallback(callback, delay) {
const timeoutRef = useRef(null);
const debouncedCallback = useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return debouncedCallback;
}
// Использование
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// API call с debouncedQuery
fetchResults(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Debounce в Go (для сравнения):
package main
import (
"fmt"
"sync"
"time"
)
type Debouncer struct {
mu sync.Mutex
timer *time.Timer
duration time.Duration
}
func NewDebouncer(duration time.Duration) *Debouncer {
return &Debouncer{duration: duration}
}
func (d *Debouncer) Debounce(fn func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.duration, fn)
}
func main() {
debouncer := NewDebouncer(300 * time.Millisecond)
for i := 0; i < 5; i++ {
value := i
debouncer.Debounce(func() {
fmt.Println("Executed:", value)
})
time.Sleep(100 * time.Millisecond)
}
time.Sleep(1 * time.Second)
// Output: Executed: 4
}
Отличие от throttle:
// Throttle — выполняется не чаще раза в N миллисекунд
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Debounce — выполняется после паузы в N миллисекунд
// Throttle — выполняется каждые N миллисекунд
Вопрос 15. Напишите функцию, которая выводит в консоль число и имя пользователя, используя замыкание с bind.
Таймкод: 00:26:20
Ответ собеседника: Правильный. Кандидат успешно реализовал функцию с использованием замыкания и метода bind для привязки контекста. Функция корректно выводит число 5 и имя 'Bob'.
Правильный ответ:
Ответ правильный. Вот несколько способов решения этой задачи с объяснением замыканий и bind.
Решение с bind и замыканием:
function createLogger(number, userName) {
// Замыкание: функция запоминает number и userName
function logInfo() {
console.log(`Number: ${number}, User: ${userName}`);
}
// Привязываем контекст с помощью bind
// bind создаёт новую функцию с привязанным this
return logInfo.bind({ number, userName });
}
// Использование
const logger = createLogger(5, 'Bob');
logger(); // Number: 5, User: Bob
Альтернативные решения:
1. Bind с передачей аргументов:
function logInfo(number, userName) {
console.log(`Number: ${number}, User: ${userName}`);
}
// bind фиксирует аргументы (partial application)
const boundLog = logInfo.bind(null, 5, 'Bob');
boundLog(); // Number: 5, User: Bob
2. Bind с контекстом:
function logInfo() {
console.log(`Number: ${this.number}, User: ${this.userName}`);
}
const context = { number: 5, userName: 'Bob' };
const boundLog = logInfo.bind(context);
boundLog(); // Number: 5, User: Bob
3. Замыкание без bind:
function createLogger(number, userName) {
// Замыкание запоминает переменные из внешней области
return function() {
console.log(`Number: ${number}, User: ${userName}`);
};
}
const logger = createLogger(5, 'Bob');
logger(); // Number: 5, User: Bob
4. Комбинация bind и замыкания:
function createLogger(number, userName) {
function logInfo() {
// this привязан через bind
// number и userName доступны через замыкание
console.log(`Closure: ${number}, ${userName}`);
console.log(`Context: ${this.number}, ${this.userName}`);
}
return logInfo.bind({ number: 100, userName: 'ContextName' });
}
const logger = createLogger(5, 'Bob');
logger();
// Closure: 5, Bob
// Context: 100, ContextName
Что такое bind:
bind() создаёт новую функцию с:
- Привязанным значением
this - Зафиксированными аргументами (partial application)
const obj = {
name: 'Alice',
greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
};
// bind создаёт новую функцию с привязанным this
const boundGreet = obj.greet.bind(obj);
boundGreet('Hello'); // Hello, Alice!
// bind с фиксированным первым аргументом
const sayHello = obj.greet.bind(obj, 'Hello');
sayHello(); // Hello, Alice!
Что такое замыкание:
Замыкание — это функция, которая запоминает переменные из внешней области видимости, даже после того как внешняя функция завершила выполнение.
function outer(x) {
// x — локальная переменная outer
function inner() {
// inner имеет доступ к x через замыкание
console.log(x);
}
return inner;
}
const fn = outer(42);
fn(); // 42 — замыкание сохранило x
Разница между bind, call и apply:
function logInfo() {
console.log(this.name, this.age);
}
const user = { name: 'Alice', age: 30 };
// bind — создаёт новую функцию (не вызывает сразу)
const boundFn = logInfo.bind(user);
boundFn();
// call — вызывает сразу с переданным this и аргументами
logInfo.call(user);
// apply — вызывает сразу с переданным this и массивом аргументов
logInfo.apply(user, []);
Вопрос 16. Пройдитесь по древовидной структуре и соберите все значения в массив.
Таймкод: 00:28:23
Ответ собеседника: Правильный. Кандидат реализовал обход дерева с использованием стека (итеративный подход). Алгоритм корректно проходит по всем узлам дерева и собирает значения в массив. Также отмечено, что для больших деревьев предпочтительнее использовать стек, а не рекурсию, чтобы избежать переполнения стека вызовов.
Правильный ответ:
Ответ правильный. Вот полная реализация различных способов обхода дерева с объяснениями.
Структура дерева:
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
}
// Пример дерева:
// 1
// / | \
// 2 3 4
// /| |
// 5 6 7
const tree = new TreeNode(1, [
new TreeNode(2, [
new TreeNode(5),
new TreeNode(6)
]),
new TreeNode(3),
new TreeNode(4, [
new TreeNode(7)
])
]);
1. Рекурсивный обход (DFS — Depth-First Search):
function collectValuesRecursive(node) {
if (!node) return [];
const result = [node.value];
for (const child of node.children) {
result.push(...collectValuesRecursive(child));
}
return result;
}
console.log(collectValuesRecursive(tree));
// [1, 2, 5, 6, 3, 4, 7]
2. Итеративный обход с стеком (DFS):
function collectValuesIterativeDFS(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
result.push(node.value);
// Добавляем детей в обратном порядке для сохранения порядка обхода
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
return result;
}
console.log(collectValuesIterativeDFS(tree));
// [1, 2, 5, 6, 3, 4, 7]
3. Обход в ширину (BFS — Breadth-First Search):
function collectValuesBFS(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length > 0) {
const node = queue.shift();
result.push(node.value);
for (const child of node.children) {
queue.push(child);
}
}
return result;
}
console.log(collectValuesBFS(tree));
// [1, 2, 3, 4, 5, 6, 7]
4. Обход с отслеживанием глубины:
function collectValuesWithDepth(root) {
if (!root) return [];
const result = [];
const stack = [{ node: root, depth: 0 }];
while (stack.length > 0) {
const { node, depth } = stack.pop();
result.push({
value: node.value,
depth: depth
});
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push({
node: node.children[i],
depth: depth + 1
});
}
}
return result;
}
console.log(collectValuesWithDepth(tree));
// [
// { value: 1, depth: 0 },
// { value: 2, depth: 1 },
// { value: 5, depth: 2 },
// { value: 6, depth: 2 },
// { value: 3, depth: 1 },
// { value: 4, depth: 1 },
// { value: 7, depth: 2 }
// ]
Реализация на Go:
package main
import "fmt"
type TreeNode struct {
Value int
Children []*TreeNode
}
func NewTreeNode(value int, children ...*TreeNode) *TreeNode {
return &TreeNode{
Value: value,
Children: children,
}
}
// Рекурсивный обход
func CollectValuesRecursive(node *TreeNode) []int {
if node == nil {
return nil
}
result := []int{node.Value}
for _, child := range node.Children {
result = append(result, CollectValuesRecursive(child)...)
}
return result
}
// Итеративный обход (DFS)
func CollectValuesIterativeDFS(root *TreeNode) []int {
if root == nil {
return nil
}
var result []int
stack := []*TreeNode{root}
for len(stack) > 0 {
// Извлекаем из стека
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = append(result, node.Value)
// Добавляем детей в обратном порядке
for i := len(node.Children) - 1; i >= 0; i-- {
stack = append(stack, node.Children[i])
}
}
return result
}
// Обход в ширину (BFS)
func CollectValuesBFS(root *TreeNode) []int {
if root == nil {
return nil
}
var result []int
queue := []*TreeNode{root}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node.Value)
queue = append(queue, node.Children...)
}
return result
}
func main() {
tree := NewTreeNode(1,
NewTreeNode(2,
NewTreeNode(5),
NewTreeNode(6),
),
NewTreeNode(3),
NewTreeNode(4,
NewTreeNode(7),
),
)
fmt.Println("Recursive DFS:", CollectValuesRecursive(tree))
fmt.Println("Iterative DFS:", CollectValuesIterativeDFS(tree))
fmt.Println("BFS:", CollectValuesBFS(tree))
}
Когда использовать каждый подход:
| Подход | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
| Рекурсия | Простой код | Stack overflow на глубоких деревьях | Небольшие деревья, простота кода |
| Итеративный DFS | Нет риска stack overflow | Чуть сложнее код | Глубокие деревья, большие объёмы |
| BFS | Находит кратчайший путь | Больше потребление памяти | Поиск по уровням, кратчайший путь |
Оптимизация для очень больших деревьев:
// Используем генератор для ленивого обхода
function* traverseTree(root) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
yield node.value;
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
// Использование — значения генерируются по требованию
const generator = traverseTree(tree);
for (const value of generator) {
console.log(value);
}
Вопрос 17. Найдите и исправьте ошибки в коде React-компонента.
Таймкод: 00:32:31
Ответ собеседника: Правильный. Кандидат нашёл и исправил несколько ошибок: мутация состояния (замена на map), отсутствие key в списке, неоптимальное создание функции в рендере (useCallback или вынос за компонент).
Правильный ответ:
Ответ правильный. Вот типовые ошибки в React-компонентах и способы их исправления.
1. Мутация состояния:
// ❌ Неправильно — мутация состояния
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build app', completed: false }
]);
const toggleTodo = (id) => {
// Мутация объекта!
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed; // ❌
setTodos(todos); // React не видит изменение
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}
// ✅ Правильно — создание нового объекта
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build app', completed: false }
]);
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // Новый объект
: todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}
2. Отсутствие key или использование index как key:
// ❌ Неправильно — нет key
{todos.map(todo => (
<li>{todo.text}</li>
))}
// ❌ Плохо — index как key (проблемы при сортировке/удалении)
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
// ✅ Правильно — уникальный стабильный key
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
3. Создание функций в рендере:
// ❌ Неправильно — новая функция при каждом рендере
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child onClick={() => console.log('clicked')} />
{/* Новая функция при каждом рендере */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// ✅ Правильно — useCallback для мемоизации
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Зависимости не меняются
const increment = useCallback(() => {
setCount(c => c + 1); // Функциональное обновление
}, []);
return (
<div>
<Child onClick={handleClick} />
<button onClick={increment}>+</button>
</div>
);
}
4. Отсутствие зависимостей в useEffect:
// ❌ Неправильно — бесконечный цикл или устаревшие данные
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // ❌ userId не в зависимостях
return <div>{user?.name}</div>;
}
// ✅ Правильно — все зависимости указаны
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; }; // Cleanup
}, [userId]); // ✅ Правильные зависимости
return <div>{user?.name}</div>;
}
5. Условный рендер с числами:
// ❌ Неправильно — 0 отрендерится как текст
function Badge({ count }) {
return (
<div>
{count && <span>{count}</span>}
{/* Если count === 0, отрендерится "0" */}
</div>
);
}
// ✅ Правильно — явная проверка
function Badge({ count }) {
return (
<div>
{count > 0 && <span>{count}</span>}
</div>
);
}
6. Замыкание на устаревшем состоянии:
// ❌ Неправильно — stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Всегда 0!
setCount(count + 1); // Всегда 1!
}, 1000);
return () => clearInterval(timer);
}, []); // count не в зависимостях
return <div>{count}</div>;
}
// ✅ Правильно — функциональное обновление
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // ✅ Используем актуальное значение
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
7. Утечка памяти — отсутствие cleanup:
// ❌ Неправильно — подписка без отписки
function Chat({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// Нет cleanup!
}, [roomId]);
}
// ✅ Правильно — cleanup функция
function Chat({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect(); // Cleanup
};
}, [roomId]);
}
Исправленный компонент (пример):
import React, { useState, useCallback, useMemo } from 'react';
// Вынесено за компонент — не создаётся заново
const STATIC_OPTIONS = ['Option 1', 'Option 2', 'Option 3'];
function OptimizedList() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1', active: false },
{ id: 2, name: 'Item 2', active: true },
{ id: 3, name: 'Item 3', active: false }
]);
const [filter, setFilter] = useState('');
// Мемоизация отфильтрованного списка
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Мемоизация обработчика
const toggleItem = useCallback((id) => {
setItems(prevItems =>
prevItems.map(item =>
item.id === id
? { ...item, active: !item.active }
: item
)
);
}, []);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter..."
/>
<ul>
{filteredItems.map(item => (
<li
key={item.id}
onClick={() => toggleItem(item.id)}
style={{
fontWeight: item.active ? 'bold' : 'normal'
}}
>
{item.name}
</li>
))}
</ul>
</div>
);
}
Вопрос 18. Будут ли перерисовываться остальные 199 компонентов при нажатии кнопки, если изменился только один элемент?
Таймкод: 00:35:57
Ответ собеседника: Правильный. Да, будут перерисовываться все 200 компонентов. Чтобы предотвратить это, нужно использовать React.memo. Также отмечено, что для примитивных значений value и label будет перерисовываться только первая строка при правильном сравнении.
Правильный ответ:
Ответ правильный. Вот детальное объяснение механизма ре-рендеринга в React и способы оптимизации.
Как работает ре-рендеринг в React:
При изменении состояния в родительском компоненте React по умолчанию перерисовывает:
- Сам компонент, где изменилось состояние
- Все дочерние компоненты (без оптимизации)
// ❌ Проблема — все 200 компонентов перерисовываются
function Parent() {
const [items, setItems] = useState(
Array.from({ length: 200 }, (_, i) => ({
id: i,
name: `Item ${i}`,
count: 0
}))
);
const increment = (id) => {
setItems(items.map(item =>
item.id === id
? { ...item, count: item.count + 1 }
: item
));
};
return (
<div>
{items.map(item => (
<ChildComponent
key={item.id}
item={item}
onIncrement={increment}
/>
))}
</div>
);
}
function ChildComponent({ item, onIncrement }) {
console.log('Render:', item.id); // Все 200 перерисовываются!
return (
<div>
<span>{item.name}: {item.count}</span>
<button onClick={() => onIncrement(item.id)}>+</button>
</div>
);
}
Способы оптимизации:
1. React.memo для предотвращения лишних ре-рендеров:
// ✅ React.memo — компонент перерисуется только при изменении пропсов
const ChildComponent = React.memo(function ChildComponent({ item, onIncrement }) {
console.log('Render:', item.id); // Только изменённый!
return (
<div>
<span>{item.name}: {item.count}</span>
<button onClick={() => onIncrement(item.id)}>+</button>
</div>
);
});
// С кастомным сравнением
const ChildComponent = React.memo(
function ChildComponent({ item, onIncrement }) {
return (
<div>
<span>{item.name}: {item.count}</span>
<button onClick={() => onIncrement(item.id)}>+</button>
</div>
);
},
(prevProps, nextProps) => {
// Вернуть true если НЕ нужно перерисовывать
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name &&
prevProps.item.count === nextProps.item.count
);
}
);
2. useCallback для стабильных ссылок на функции:
function Parent() {
const [items, setItems] = useState(/* ... */);
// ❌ Новая функция при каждом рендере — React.memo не поможет
const increment = (id) => {
setItems(items.map(item =>
item.id === id ? { ...item, count: item.count + 1 } : item
));
};
// ✅ useCallback — стабильная ссылка
const increment = useCallback((id) => {
setItems(prevItems =>
prevItems.map(item =>
item.id === id ? { ...item, count: item.count + 1 } : item
)
);
}, []); // Нет зависимостей — функция создаётся один раз
return (
<div>
{items.map(item => (
<ChildComponent
key={item.id}
item={item}
onIncrement={increment}
/>
))}
</div>
);
}
3. Разделение состояния:
// ✅ Лучшее решение — вынести состояние в отдельный компонент
function Parent() {
const [items, setItems] = useState(/* ... */);
return (
<div>
{items.map(item => (
<ItemWrapper
key={item.id}
item={item}
onIncrement={(updatedItem) => {
setItems(prev =>
prev.map(i => i.id === updatedItem.id ? updatedItem : i)
);
}}
/>
))}
</div>
);
}
// Каждый элемент имеет своё состояние — родитель не перерисовывается
function ItemWrapper({ item: initialItem, onIncrement }) {
const [item, setItem] = useState(initialItem);
const handleIncrement = useCallback(() => {
const updated = { ...item, count: item.count + 1 };
setItem(updated);
onIncrement(updated);
}, [item, onIncrement]);
return (
<div>
<span>{item.name}: {item.count}</span>
<button onClick={handleIncrement}>+</button>
</div>
);
}
4. Виртуализация списка для очень больших данных:
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items, onIncrement }) {
const Row = useCallback(({ index, style }) => {
const item = items[index];
return (
<div style={style}>
<span>{item.name}: {item.count}</span>
<button onClick={() => onIncrement(item.id)}>+</button>
</div>
);
}, [items, onIncrement]);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
5. Context с селекторами:
// ❌ Проблема — все потребители перерисовываются при любом изменении
const ItemsContext = createContext();
function Parent() {
const [items, setItems] = useState(/* ... */);
return (
<ItemsContext.Provider value={{ items, setItems }}>
{items.map(item => (
<ChildComponent key={item.id} id={item.id} />
))}
</ItemsContext.Provider>
);
}
function ChildComponent({ id }) {
const { items } = useContext(ItemsContext); // Перерисовка при любом изменении!
const item = items.find(i => i.id === id);
return <div>{item.name}</div>;
}
// ✅ Решение — использовать библиотеку с селекторами
import { useSelector } from 'react-redux';
// или
import { useSyncExternalStore } from 'react';
function ChildComponent({ id }) {
const item = useSelector(state => state.items.find(i => i.id === id));
return <div>{item.name}</div>;
}
Итоговые рекомендации:
| Подход | Когда использовать |
|---|---|
| React.memo | Дочерние компоненты получают пропсы от родителя |
| useCallback | Функции передаются как пропсы в memoized компоненты |
| useMemo | Вычисляемые значения передаются как пропсы |
| Разделение состояния | Каждый элемент списка имеет своё состояние |
| Виртуализация | Большие списки (1000+ элементов) |
| Context + селекторы | Глобальное состояние с множеством потребителей |
Важно помнить:
- React.memo — не бесплатный, есть накладные расходы на сравнение
- Не оптимизируйте преждевременно — профилируйте сначала
- React DevTools Profiler — лучший инструмент для поиска узких мест
Вопрос 19. Чем отличается тип 'any' от 'unknown' в TypeScript?
Таймкод: 00:38:17
Ответ собеседника: Правильный. any отключает проверку типов, позволяя любые операции. unknown — безопасная альтернатива, требующая проверки типа перед использованием.
Правильный ответ:
Ответ правильный. Вот детальное сравнение any и unknown с примерами.
any — полное отключение типизации:
let value: any;
// Всё разрешено — TypeScript не проверяет
value = 42;
value = "hello";
value = { name: "John" };
// Любые операции без ошибок компиляции
value.foo(); // ✅ Нет ошибки — будет runtime error
value[0]; // ✅ Нет ошибки
value + 1; // ✅ Нет ошибки
const x: number = value; // ✅ Нет ошибки — any совместим со всеми типами
unknown — безопасная версия any:
let value: unknown;
// Присвоение любого значения — разрешено
value = 42;
value = "hello";
value = { name: "John" };
// Но использование без проверки типа — ошибка
value.foo(); // ❌ Error: Object is of type 'unknown'
value[0]; // ❌ Error
value + 1; // ❌ Error
const x: number = value; // ❌ Error: Type 'unknown' is not assignable to type 'number'
Работа с unknown — type narrowing:
function processValue(value: unknown) {
// 1. Type guard с typeof
if (typeof value === 'string') {
console.log(value.toUpperCase()); // ✅ string
}
// 2. Type guard с instanceof
if (value instanceof Date) {
console.log(value.getFullYear()); // ✅ Date
}
// 3. Type guard с проверкой свойств
if (value && typeof value === 'object' && 'name' in value) {
console.log((value as { name: string }).name);
}
// 4. Type assertion (осторожно!)
const str = value as string; // Может вызвать runtime error
// 5. Type predicate
if (isUser(value)) {
console.log(value.name); // ✅ User
}
}
// Type predicate
function isUser(value: unknown): value is { name: string; age: number } {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'age' in value &&
typeof (value as any).name === 'string' &&
typeof (value as any).age === 'number'
);
}
Сравнительная таблица:
| Характеристика | any | unknown |
|---|---|---|
| Присвоение любого значения | ✅ | ✅ |
| Использование без проверки | ✅ | ❌ |
| Вызов методов | ✅ | ❌ |
| Приведение к другому типу | Автоматически | Требует явного assertion |
| Безопасность | Низкая | Высокая |
| Рекомендация | Избегать | Использовать вместо any |
Практические примеры:
// Обработка JSON из API
async function fetchData(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json(); // Возвращает unknown
}
async function main() {
const data = await fetchData('/api/user');
// ❌ Неправильно — any
const userAny: any = data;
console.log(userAny.name); // Runtime error если структура другая
// ✅ Правильно — unknown с проверкой
if (isValidUser(data)) {
console.log(data.name); // Безопасно
}
}
function isValidUser(data: unknown): data is { name: string; email: string } {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
'email' in data
);
}
// Обработка ошибок
try {
// ...
} catch (error: unknown) {
// error — unknown, не any
if (error instanceof Error) {
console.log(error.message); // ✅
} else if (typeof error === 'string') {
console.log(error); // ✅
} else {
console.log('Unknown error', error);
}
}
// Generic вместо any
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T = number
const str = identity("hello"); // T = string
// Лучше чем any — сохраняется информация о типе
Когда использовать:
- unknown — когда тип заранее неизвестен (JSON из API, ошибки, пользовательский ввод)
- any — только для миграции с JavaScript или работы с нетипизированными библиотеками
- generics — когда нужно сохранить тип, но он может быть разным
Правило: Всегда предпочитайте unknown вместо any. Это заставляет вас явно проверять тип перед использованием, что предотвращает ошибки в runtime.
Вопрос 20. С какими стейт-менеджерами в связке с React вам доводилось использовать?
Таймкод: 00:40:01
Ответ собеседника: Правильный. Работал с MobX, Effector, Redux Toolkit. Redux Toolkit понравился — иммер под капотом, entityAdapter для нормализации, кэширование, long polling. В связке с GraphQL удобно использовать генераторы типов.
Правильный ответ:
Ответ правильный и демонстрирует опыт работы с различными подходами к управлению состоянием. Вот более детальный разбор основных стейт-менеджеров.
Redux Toolkit (RTK):
Современный стандарт для Redux — значительно упрощает работу по сравнению с классическим Redux.
import { createSlice, createAsyncThunk, configureStore, createEntityAdapter } from '@reduxjs/toolkit';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
// Entity Adapter для нормализации данных
const usersAdapter = createEntityAdapter<User>({
selectId: (user) => user.id,
sortComparer: (a, b) => a.name.localeCompare(b.name)
});
// Async Thunk для загрузки данных
export const fetchUsers = createAsyncThunk(
'users/fetchAll',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
} catch (error) {
return rejectWithValue((error as Error).message);
}
}
);
// Slice с редьюсерами и экшенами
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState({
loading: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
error: null as string | null
}),
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
userRemoved: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'succeeded';
usersAdapter.setAll(state, action.payload);
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload as string;
});
}
});
// RTK Query для кэширования и автоматических запросов
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User']
}),
getUser: builder.query<User, string>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }]
}),
addUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body
}),
invalidatesTags: ['User'] // Автоматический рефетч
}),
updateUser: builder.mutation<User, Partial<User> & { id: string }>({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }]
}),
deleteUser: builder.mutation<void, string>({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
}),
invalidatesTags: ['User']
})
})
});
export const {
useGetUsersQuery,
useGetUserQuery,
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation
} = apiSlice;
// Store
export const store = configureStore({
reducer: {
users: usersAdapter,
[apiSlice.reducerPath]: apiSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware)
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Типизированные хуки
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Селекторы из Entity Adapter
export const {
selectAll: selectAllUsers,
selectById: selectUserById,
selectIds: selectUserIds
} = usersAdapter.getSelectors<RootState>((state) => state.users);
MobX:
Реактивный стейт-менеджер на основе паттерна Observer.
import { makeAutoObservable, runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
class UserStore {
users: User[] = [];
loading = false;
error: string | null = null;
constructor() {
makeAutoObservable(this);
}
get activeUsers() {
return this.users.filter(u => u.isActive);
}
get userCount() {
return this.users.length;
}
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
const data = await response.json();
runInAction(() => {
this.users = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = (error as Error).message;
this.loading = false;
});
}
}
addUser(user: User) {
this.users.push(user);
}
removeUser(id: string) {
this.users = this.users.filter(u => u.id !== id);
}
}
export const userStore = new UserStore();
// React компонент
const UserList = observer(() => {
const { users, loading, activeUsers, fetchUsers } = userStore;
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
});
Zustand:
Минималистичный стейт-менеджер без boilerplate.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface UserState {
users: User[];
loading: boolean;
fetchUsers: () => Promise<void>;
addUser: (user: User) => void;
removeUser: (id: string) => void;
}
export const useUserStore = create<UserState>()(
devtools(
persist(
immer((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true });
const response = await fetch('/api/users');
const users = await response.json();
set((state) => {
state.users = users;
state.loading = false;
});
},
addUser: (user) => {
set((state) => {
state.users.push(user);
});
},
removeUser: (id) => {
set((state) => {
state.users = state.users.filter(u => u.id !== id);
});
}
})),
{ name: 'user-storage' }
)
)
);
// Использование в компоненте
function UserList() {
const users = useUserStore((state) => state.users);
const fetchUsers = useUserStore((state) => state.fetchUsers);
useEffect(() => {
fetchUsers();
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Сравнение стейт-менеджеров:
| Характеристика | Redux Toolkit | MobX | Zustand | Effector |
|---|---|---|---|---|
| Boilerplate | Средний | Низкий | Минимальный | Средний |
| Кривая обучения | Высокая | Средняя | Низкая | Средняя |
| DevTools | Отличные | Хорошие | Хорошие | Хорошие |
| TypeScript | Отличная | Хорошая | Отличная | Отличная |
| Middleware | Да | Нет | Да | Да |
| Размер | ~11KB | ~16KB | ~1KB | ~2KB |
| Реактивность | Нет | Да | Нет | Да |
| Экосистема | Огромная | Большая | Растущая | Средняя |
Рекомендации:
- Redux Toolkit — для больших приложений с командой, стандарт индустрии
- Zustand — для небольших/средних проектов, минимум boilerplate
- MobX — если нравится реактивный подход и OOP
- Effector — для сложной бизнес-логики с множеством связей между состояниями
- React Query / SWR — для серверного состояния (часто заменяет стейт-менеджер)
Вопрос 21. Как вы следите за чистотой кода? Какие линтеры используете?
Таймкод: 00:41:00
Ответ собеседника: Правильный. Использует линтеры для статического анализа и форматирования. Есть опыт написания собственных линтеров с работой с AST. Обычно берёт стандартные конфигурации и подстраивает. Для React-проектов — официальный плагин ESLint с правилами для хуков.
Правильный ответ:
Ответ правильный и демонстрирует глубокое понимание инструментов. Вот более полный разбор экосистемы линтеров и форматеров.
ESLint — основной линтер для JavaScript/TypeScript:
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true
},
project: './tsconfig.json'
},
env: {
browser: true,
es2022: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier' // Должен быть последним — отключает конфликтующие правила
],
plugins: [
'@typescript-eslint',
'react',
'react-hooks',
'jsx-a11y',
'import',
'unused-imports',
'simple-import-sort'
],
rules: {
// TypeScript
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/strict-boolean-expressions': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': 'error',
// React
'react/react-in-jsx-scope': 'off', // Не нужен в React 17+
'react/prop-types': 'off', // Используем TypeScript
'react/jsx-no-target-blank': 'error',
'react/self-closing-comp': 'error',
// React Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import
'import/no-cycle': 'error',
'import/no-unused-modules': 'warn',
'unused-imports/no-unused-imports': 'error',
'simple-import-sort/imports': ['error', {
groups: [
// Side effect imports
['^\\u0000'],
// Node.js builtins
['^node:'],
// Packages
['^@?\\w'],
// Internal packages
['^@/'],
// Parent imports
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Same-layer imports
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)'],
// Style imports
['^.+\\.s?css$']
]
}],
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'no-implicit-coercion': 'error'
},
settings: {
'react': {
version: 'detect'
},
'import/resolver': {
typescript: {
alwaysTryTypes: true
}
}
},
overrides: [
{
files: ['*.test.ts', '*.test.tsx', '*.spec.ts'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'no-console': 'off'
}
}
]
};
Prettier — форматирование кода:
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"jsxSingleQuote": false
}
Написание собственного ESLint-правила с AST:
// eslint-plugin-custom/rules/no-api-call-in-render.js
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow direct API calls in render method',
category: 'Best Practices',
recommended: true
},
fixable: null,
schema: [],
messages: {
noApiCallInRender: 'Avoid direct API calls in render. Use useEffect or event handlers.'
}
},
create(context) {
// Паттерны API-вызовов для поиска
const apiPatterns = [
/fetch\(/,
/axios\./,
/\$http\./,
/api\./,
/request\(/
];
function isInsideRender(node) {
let parent = node.parent;
while (parent) {
// Проверяем, находимся ли мы внутри компонента
if (
parent.type === 'FunctionDeclaration' ||
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression'
) {
const functionName = getFunctionName(parent);
if (functionName && /^[A-Z]/.test(functionName)) {
return true; // Это React-компонент
}
}
parent = parent.parent;
}
return false;
}
function isInsideCallback(node) {
let parent = node.parent;
while (parent) {
if (
parent.type === 'CallExpression' &&
parent.callee.property &&
['useEffect', 'useCallback', 'useMemo', 'then', 'catch', 'finally', 'map', 'filter', 'forEach'].includes(parent.callee.property.name)
) {
return true;
}
parent = parent.parent;
}
return false;
}
return {
CallExpression(node) {
const code = context.getSourceCode().getText(node);
const isApiCall = apiPatterns.some(pattern => pattern.test(code));
if (isApiCall && isInsideRender(node) && !isInsideCallback(node)) {
context.report({
node,
messageId: 'noApiCallInRender'
});
}
}
};
}
};
golangci-lint для Go-проектов:
# .golangci.yml
run:
timeout: 5m
go: '1.21'
skip-dirs:
- vendor
- third_party
- testdata
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 15
gocognit:
min-complexity: 20
dupl:
threshold: 100
lll:
line-length: 120
funlen:
lines: 100
statements: 50
nestif:
min-complexity: 5
errcheck:
check-type-assertions: true
check-blank: true
staticcheck:
go: '1.21'
checks: ['all', '-ST1000', '-ST1003']
revive:
rules:
- name: var-naming
disabled: true
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
- asciicheck
- bodyclose
- cyclop
- dupl
- durationcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- gofmt
- gofumpt
- goheader
- goimports
- gomnd
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- importas
- lll
- makezero
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- predeclared
- promlinter
- reassign
- revive
- rowserrcheck
- sqlclosecheck
- stylecheck
- tenv
- testableexamples
- thelper
- tparallel
- unconvert
- unparam
- usestdlibvars
- wastedassign
- whitespace
disable:
- depguard
- gci
- godox
- goerr113
- ireturn
- nlreturn
- varnamelen
- wsl
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- funlen
- dupl
- gosec
- path: _test\.go
text: "weak cryptographic primitive"
Pre-commit хуки с Husky и lint-staged:
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
],
"*.go": [
"gofmt -w",
"goimports -w"
]
}
}
Commitlint — проверка сообщений коммитов:
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // Новая функциональность
'fix', // Исправление бага
'docs', // Документация
'style', // Форматирование
'refactor', // Рефакторинг
'perf', // Оптимизация производительности
'test', // Тесты
'build', // Сборка
'ci', // CI/CD
'chore', // Обслуживание
'revert' // Откат
]
],
'subject-case': [2, 'always', 'lower-case'],
'subject-max-length': [2, 'always', 100]
}
};
CI/CD проверки:
# .github/workflows/lint.yml
name: Lint
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm run format:check
# Go linting
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=5m
Рекомендуемый стек:
| Инструмент | Назначение |
|---|---|
| ESLint | Линтер для JS/TS |
| Prettier | Форматирование кода |
| TypeScript | Статическая типизация |
| Husky | Git хуки |
| lint-staged | Линтинг только изменённых файлов |
| commitlint | Проверка сообщений коммитов |
| golangci-lint | Линтер для Go |
| go vet | Встроенный анализатор Go |
| goimports | Форматирование импортов Go |
