Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Backend · Уровень: Middle+/Senior ## TL;DR - **Middleware** в идиоматическом Go — это функция `func(http.Handler) http.Handler`: она принимает следующий хендлер, оборачивает его и возвращает новый `http.Handler`. Это декоратор поверх интерфейса `http.Handler`. - **Цепочка** строится композицией: `Chain(m1, m2, m3)(final)`. Внешний middleware оборачивает внутренний. На входе порядок прямой (m1 → m2 → m3 → handler), на выходе — **LIFO** (handler → m3 → m2 → m1), потому что код после `next.ServeHTTP` выполняется при разворачивании стека. - **Захват status code** требует обёртки `http.ResponseWriter`, т.к. интерфейс не отдаёт код наружу. Наивная обёртка **теряет** `http.Flusher`, `http.Hijacker`, `http.Pusher` — это классический баг. - **Порядок критичен**: `recovery` — самым снаружи (ловит панику из всех остальных), `CORS` — рано (чтобы preflight отработал до auth), `auth` — до бизнес-логики, `rate limit` — обычно до тяжёлой работы. - **Best practices**: middleware должны быть дешёвыми, не блокировать, идемпотентными по эффекту, передавать данные через `context.Context` (не через мутацию структур), не хардкодить зависимости (внедрять через замыкание/конструктор). - В `net/http` 1.22+ роутер `http.ServeMux` умеет методы и path-параметры, но **не** имеет встроенной поддержки middleware-цепочек — оборачивать `mux` целиком или использовать `chi`. ## Теория ### 1. Каноническое определение Middleware — это декоратор над `http.Handler`. Базовая сигнатура: ```go type Middleware func(next http.Handler) http.Handler ``` Простейший пример — middleware, добавляющий заголовок: ```go func AddHeader(key, value string) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(key, value) next.ServeHTTP(w, r) // вызов следующего звена }) } } ``` Ключевые моменты: - Возвращаемое значение — `http.Handler` (через адаптер `http.HandlerFunc`). - `next.ServeHTTP(w, r)` — точка передачи управления дальше по цепочке. - Код **до** `next.ServeHTTP` выполняется на «входе», код **после** — на «выходе» (разворачивание стека). - Можно **не вызывать** `next` (например, при провале auth или rate limit) — тогда цепочка прерывается (short-circuit). ### 2. Цепочки: ручное вложение vs хелпер **Ручное вложение** читается «наизнанку» и плохо масштабируется: ```go handler := Logging(Recovery(Auth(myHandler))) // Выполнение на входе: Logging -> Recovery -> Auth -> myHandler ``` **Хелпер `Chain`** делает порядок объявления = порядку выполнения: ```go func Chain(mws ...Middleware) Middleware { return func(final http.Handler) http.Handler { // Оборачиваем с конца, чтобы первый в списке оказался самым внешним. for i := len(mws) - 1; i >= 0; i-- { final = mws[i](final) } return final } } // Использование: Logging — самый внешний, Auth — ближе к хендлеру. h := Chain(Logging, Recovery, Auth)(myHandler) ``` > Важно: порядок цикла (`len-1 → 0`) выбран так, чтобы `mws[0]` обернул всё остальное. Если итерировать с начала — порядок инвертируется. Это типовая ошибка в самописных `Chain`. ### 3. Порядок выполнения и LIFO на выходе Рассмотрим цепочку `Chain(A, B, C)(H)`: ``` Запрос → A(before) → B(before) → C(before) → H → C(after) → B(after) → A(after) → Ответ ``` - **Вход (before)**: прямой порядок A → B → C (внешний → внутренний). - **Выход (after)**: обратный порядок C → B → A (**LIFO**, как стек вызовов). Это прямое следствие того, что `after`-код стоит после `next.ServeHTTP` и выполняется при возврате из рекурсии вызовов. ```go func Trace(name string) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("-> %s", name) // before next.ServeHTTP(w, r) log.Printf("<- %s", name) // after (LIFO) }) } } // Chain(Trace("A"), Trace("B"))(h) // Вывод: -> A, -> B, (handler), <- B, <- A ``` Практический вывод: если middleware измеряет общую длительность (timing), его `after`-код должен видеть результат всех вложенных — значит он должен быть **снаружи**. ### 4. Захват status code: обёртка ResponseWriter Интерфейс `http.ResponseWriter` **не предоставляет** способа прочитать записанный статус-код или количество байт — он только пишет. Поэтому для логирования статуса нужна обёртка: ```go type statusRecorder struct { http.ResponseWriter status int bytes int wroteHeader bool } func (r *statusRecorder) WriteHeader(code int) { if r.wroteHeader { return // защита от двойного WriteHeader (иначе паника в net/http) } r.status = code r.wroteHeader = true r.ResponseWriter.WriteHeader(code) } func (r *statusRecorder) Write(b []byte) (int, error) { if !r.wroteHeader { // Имитируем поведение net/http: первый Write без WriteHeader => 200. r.WriteHeader(http.StatusOK) } n, err := r.ResponseWriter.Write(b) r.bytes += n return n, err } ``` Нюансы: - Если хендлер ни разу не вызвал `WriteHeader` и не писал тело — статус-код останется нулём; инициализируйте `status` значением `http.StatusOK` или обрабатывайте `0` как 200. - `Write` неявно выставляет 200 при первой записи — это надо повторить, иначе статус для тел без явного `WriteHeader` будет неверным. - Защита от повторного `WriteHeader` важна: стандартный `net/http` логирует `superfluous response.WriteHeader call`, а ваша обёртка может затереть корректный статус. ### 5. Проблема потери интерфейсов (Flusher/Hijacker/Pusher) `http.ResponseWriter` от сервера **дополнительно** реализует опциональные интерфейсы: | Интерфейс | Назначение | Кто использует | |---|---|---| | `http.Flusher` | принудительный сброс буфера | SSE, стриминг, chunked | | `http.Hijacker` | захват TCP-соединения | WebSocket (gorilla/ws, nhooyr) | | `http.Pusher` | HTTP/2 Server Push | HTTP/2 push (deprecated в браузерах) | | `io.ReaderFrom` | оптимизация копирования (sendfile) | отдача файлов | Когда вы оборачиваете writer структурой `statusRecorder` со встроенным `http.ResponseWriter`, **встраивание промотит только методы интерфейса `http.ResponseWriter`** (`Header`, `Write`, `WriteHeader`). Type assertion `w.(http.Flusher)` на вашей обёртке **провалится**, даже если оригинальный writer реализует Flusher — потому что обёртка как тип не объявляет этих методов через свой статический интерфейс... Точнее: встроенное поле промотит и `Flush`, если базовый writer его имеет, **но только если базовое поле имеет статический тип, реализующий Flusher**. Поскольку поле имеет тип `http.ResponseWriter` (интерфейс без Flush), методы Flusher НЕ промотятся, и assertion `recorder.(http.Flusher)` вернёт `false`. Это и есть потеря интерфейсов. ```go // БАГ: SSE/WebSocket перестают работать за этим middleware. rec := &statusRecorder{ResponseWriter: w} next.ServeHTTP(rec, r) // внутри handler: w.(http.Flusher) -> ok == false ``` Решения: **A. Явно проксировать нужные методы** (если знаете, что они есть): ```go func (r *statusRecorder) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { f.Flush() } } func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { if h, ok := r.ResponseWriter.(http.Hijacker); ok { return h.Hijack() } return nil, nil, fmt.Errorf("ResponseWriter does not support Hijack") } func (r *statusRecorder) Push(target string, opts *http.PushOptions) error { if p, ok := r.ResponseWriter.(http.Pusher); ok { return p.Push(target, opts) } return http.ErrNotSupported } ``` **B. Использовать готовое решение** — `github.com/felixge/httpsnoop`, которое динамически собирает writer, сохраняющий ровно тот набор опциональных интерфейсов, что был у оригинала (через комбинаторику типов): ```go m := httpsnoop.CaptureMetrics(next, w, r) // m.Code, m.Duration, m.Written ``` Это де-факто стандартный способ корректного захвата метрик без потери интерфейсов. ### 6. Типовые middleware #### 6.1 Logging (с захватом status code) ```go func Logging(logger *slog.Logger) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) logger.Info("http_request", "method", r.Method, "path", r.URL.Path, "status", rec.status, "bytes", rec.bytes, "duration_ms", time.Since(start).Milliseconds(), "remote", r.RemoteAddr, ) }) } } ``` #### 6.2 Recovery (recover от паники → 500) ```go func Recovery(logger *slog.Logger) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { // ErrAbortHandler — спец-паника net/http, её не логируем как ошибку if rec == http.ErrAbortHandler { panic(rec) } logger.Error("panic recovered", "err", rec, "stack", string(debug.Stack()), ) // Если заголовки ещё не отправлены — отдаём 500. w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("internal server error")) } }() next.ServeHTTP(w, r) }) } } ``` Нюансы: - `recover` ловит панику **только в той же горутине**. Паника в `go func(){...}()` внутри хендлера обрушит весь процесс — middleware не поможет. - Если хендлер уже успел вызвать `WriteHeader`, повторный `WriteHeader(500)` ничего не изменит (статус уже на проводе) и вызовет `superfluous WriteHeader`. Это допустимо, но статус клиенту уйдёт старый. - `http.ErrAbortHandler` следует пробрасывать дальше — это штатный механизм прерывания. #### 6.3 Auth (проверка токена → user в context) ```go type ctxKey int const userKey ctxKey = iota type User struct { ID string Roles []string } func Auth(verify func(token string) (*User, error)) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authz := r.Header.Get("Authorization") token, ok := strings.CutPrefix(authz, "Bearer ") if !ok || token == "" { http.Error(w, "unauthorized", http.StatusUnauthorized) return // short-circuit: next НЕ вызывается } user, err := verify(token) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), userKey, user) next.ServeHTTP(w, r.WithContext(ctx)) // прокидываем новый request }) } } // Типобезопасный геттер для хендлеров. func UserFromContext(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userKey).(*User) return u, ok } ``` Нюансы: - Ключ контекста — **приватный тип** (`ctxKey`), не `string`. Это защищает от коллизий ключей между пакетами (требование `go vet` / `staticcheck`). - Данные прокидываются через `r.WithContext(ctx)` — request иммутабелен, `WithContext` создаёт shallow-копию. - `context.Value` — только для request-scoped данных (user, trace-id, request-id), не для передачи опциональных параметров функций. #### 6.4 CORS (заголовки + preflight OPTIONS) ```go func CORS(allowedOrigins []string) Middleware { allowed := make(map[string]bool, len(allowedOrigins)) for _, o := range allowedOrigins { allowed[o] = true } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && (allowed["*"] || allowed[origin]) { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") // важно для кеширования прокси/CDN w.Header().Set("Access-Control-Allow-Credentials", "true") } // Preflight: браузер шлёт OPTIONS перед "сложным" запросом. if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Max-Age", "86400") // кеш preflight w.WriteHeader(http.StatusNoContent) return // НЕ передаём дальше — preflight не доходит до auth/handler } next.ServeHTTP(w, r) }) } } ``` Нюансы: - При `Allow-Credentials: true` **нельзя** отдавать `Allow-Origin: *` — браузер отклонит. Нужно эхо конкретного `Origin`. - `Vary: Origin` обязателен, иначе CDN закеширует ответ для одного origin и отдаст другому. - Preflight (`OPTIONS`) надо обработать **до** auth: у preflight нет `Authorization`, иначе он словит 401 и реальный запрос не пойдёт. #### 6.5 Rate limiting (golang.org/x/time/rate, per-IP) ```go import "golang.org/x/time/rate" type ipLimiter struct { mu sync.Mutex limiters map[string]*entry rps rate.Limit burst int } type entry struct { lim *rate.Limiter lastSeen time.Time } func newIPLimiter(rps rate.Limit, burst int) *ipLimiter { l := &ipLimiter{limiters: map[string]*entry{}, rps: rps, burst: burst} go l.cleanup() // фоновая чистка, иначе утечка памяти по IP return l } func (l *ipLimiter) get(ip string) *rate.Limiter { l.mu.Lock() defer l.mu.Unlock() e, ok := l.limiters[ip] if !ok { e = &entry{lim: rate.NewLimiter(l.rps, l.burst)} l.limiters[ip] = e } e.lastSeen = time.Now() return e.lim } func (l *ipLimiter) cleanup() { for range time.Tick(time.Minute) { l.mu.Lock() for ip, e := range l.limiters { if time.Since(e.lastSeen) > 3*time.Minute { delete(l.limiters, ip) } } l.mu.Unlock() } } func RateLimit(l *ipLimiter) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip, _, _ := net.SplitHostPort(r.RemoteAddr) if !l.get(ip).Allow() { w.Header().Set("Retry-After", "1") http.Error(w, "rate limit exceeded", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } } ``` Нюансы: - `rate.Limiter` основан на **token bucket**: `rate.Limit` — пополнение токенов в секунду, `burst` — ёмкость ведра (допустимый всплеск). - `Allow()` неблокирующий; `Wait(ctx)` блокирует до получения токена; `Reserve()` даёт резервацию с возможным откатом. В middleware обычно `Allow()`. - За балансировщиком `r.RemoteAddr` = IP прокси. Реальный IP надо брать из `X-Forwarded-For` / `X-Real-IP`, **но только доверяя своему прокси** (иначе клиент подделает заголовок и обойдёт лимит). - Без фоновой чистки `map[ip]` растёт неограниченно — DoS по памяти. ### 7. Порядок middleware: рекомендуемая компоновка ```go h := Chain( Recovery(logger), // 1. снаружи всех — ловит панику отовсюду RequestID(), // 2. id для трассировки во всех логах Logging(logger), // 3. видит финальный статус (включая 500 от Recovery) CORS(origins), // 4. рано: preflight OPTIONS не должен дойти до auth RateLimit(limiter), // 5. до тяжёлой работы; иногда до auth, иногда после Auth(verifyToken), // 6. до бизнес-логики; кладёт user в context )(businessHandler) ``` | Middleware | Позиция | Почему | |---|---|---| | Recovery | самый внешний | должен перехватить панику любого вложенного middleware и хендлера | | RequestID | очень рано | чтобы id попал во все последующие логи | | Logging | снаружи (но внутри Recovery) | чтобы залогировать итоговый статус, включая 500 от Recovery | | CORS | рано | preflight OPTIONS обрабатывается до auth (нет токена) | | RateLimit | до бизнес-логики | дешёвый отсев; защищает дорогие ресурсы | | Auth | перед хендлером | бизнес-логике нужен готовый user в context | > Тонкость: Logging vs Recovery. Если Recovery снаружи Logging, то при панике Logging уже отработает свой `after` и залогирует статус, а Recovery поймает панику и отдаст 500 — но Logging может не увидеть этот 500. Часто Recovery ставят **и** снаружи (для отдачи 500), а метрики статуса считают через httpsnoop внутри Logging. Иногда Logging делают внешним, чтобы он точно зафиксировал даже паникующие запросы. Решение зависит от того, что важнее: гарантия 500 или гарантия лога. ### 8. Стандартные паттерны в роутерах #### chi ```go import "github.com/go-chi/chi/v5" import "github.com/go-chi/chi/v5/middleware" r := chi.NewRouter() r.Use(middleware.RequestID) // глобально, в порядке объявления r.Use(middleware.Recoverer) r.Use(middleware.Logger) r.Group(func(r chi.Router) { // изолированная группа со своими middleware r.Use(AuthMiddleware) r.Get("/me", meHandler) }) ``` - `chi` использует ту же сигнатуру `func(http.Handler) http.Handler` — ваши middleware совместимы с chi без адаптеров. - `r.Use` применяет в порядке объявления (первый — внешний). - `r.Group` / `r.Route` позволяют локальные цепочки (например, защищённые маршруты). - `middleware.WrapResponseWriter` в chi корректно сохраняет Flusher/Hijacker. #### net/http 1.22+ Роутер `http.ServeMux` с 1.22 поддерживает методы и path-параметры (`r.PathValue("id")`), но **не** middleware-цепочки. Оборачивают весь mux: ```go mux := http.NewServeMux() mux.HandleFunc("GET /users/{id}", getUser) // метод + path-param (1.22) mux.HandleFunc("POST /users", createUser) // Глобальные middleware — оборачиваем mux целиком. handler := Chain(Recovery(logger), Logging(logger))(mux) srv := &http.Server{Addr: ":8080", Handler: handler} log.Fatal(srv.ListenAndServe()) ``` Для middleware на подмножество маршрутов в чистом `net/http` — оборачивают конкретные хендлеры вручную или используют вложенные mux. ## Подводные камни / gotchas - **Потеря опциональных интерфейсов** при обёртке `ResponseWriter` (Flusher/Hijacker/Pusher) — ломает SSE и WebSocket. Используйте httpsnoop или явно проксируйте методы. - **`recover` не ловит панику из дочерних горутин** хендлера — процесс упадёт. Recovery middleware защищает только основную горутину запроса. - **Двойной `WriteHeader`** — паника/предупреждение в net/http. Обёртка должна защищаться флагом `wroteHeader`. - **Изменение порядка ломает семантику**: CORS после auth → preflight ловит 401; Logging снаружи Recovery vs внутри → разный статус в логах. - **`r.RemoteAddr` за прокси** = IP прокси. Rate limit/логи по нему бесполезны без аккуратной обработки `X-Forwarded-For` (и доверия к прокси). - **Утечка памяти в per-IP лимитере** без фоновой очистки map. - **Мутация `r.Header`/глобального состояния** в middleware — не идемпотентно при ретраях, опасно при параллельной обработке. - **`context.WithValue` со строковым ключом** — коллизии между пакетами; всегда приватный тип ключа. - **Тяжёлая работа в middleware** (синхронный запрос в БД на каждый запрос для auth без кеша) добавляет латентность ко всем эндпоинтам. - **Запись в `w` до `next` без необходимости** (например, заголовки) фиксирует header set — потом `WriteHeader` с другим кодом не сработает. - **`Allow-Origin: *` вместе с `Allow-Credentials: true`** — отклоняется браузером. - **Забытый `Vary: Origin`** — CDN отдаёт чужой CORS-ответ. ## Вопросы на собеседовании **В:** Почему идиоматическая сигнатура middleware — именно `func(http.Handler) http.Handler`, а не, скажем, `func(w, r, next)`? **О:** Потому что `http.Handler` — единственный интерфейс, который понимает весь стандартный стек (`http.Server`, `ServeMux`, сторонние роутеры). Декоратор `func(http.Handler) http.Handler` сохраняет тип, поэтому цепочки композируются без адаптеров и совместимы с chi, gorilla и т.д. Сигнатура с явным `next` (как в Express/негони) требует своего раннера и не является нативной для `net/http`. **В:** В каком порядке выполняются middleware в цепочке на входе и на выходе? **О:** На входе — прямой порядок объявления (внешний → внутренний), до вызова `next.ServeHTTP`. На выходе — обратный, LIFO, потому что код после `next.ServeHTTP` срабатывает при разворачивании стека вызовов. Поэтому таймер общей длительности и recovery должны быть снаружи. **В:** Зачем оборачивать `http.ResponseWriter` и какая главная опасность этой обёртки? **О:** Интерфейс `ResponseWriter` не отдаёт записанный статус-код/размер тела, поэтому для логирования нужна обёртка, перехватывающая `WriteHeader`/`Write`. Главная опасность — потеря опциональных интерфейсов (`Flusher`, `Hijacker`, `Pusher`, `io.ReaderFrom`): встроенное поле типа `http.ResponseWriter` не промотит их методы, и type assertion в хендлере провалится, ломая SSE/WebSocket/стриминг. Решение — проксировать методы вручную или использовать httpsnoop. **В:** Почему именно встраивание `http.ResponseWriter` теряет Flusher, ведь Go промотит методы встроенного поля? **О:** Промоушн методов работает по **статическому типу поля**. Поле имеет тип интерфейса `http.ResponseWriter`, у которого в наборе методов нет `Flush()`. Поэтому компилятор не промотит Flush, даже если динамический тип за интерфейсом его реализует. Обёртка как тип не удовлетворяет `http.Flusher`, и `obj.(http.Flusher)` → `false`. **В:** Где в цепочке должен стоять recovery middleware и почему? **О:** Самым внешним. Тогда `defer recover()` перехватит панику из любого вложенного middleware и хендлера. Если recovery вложить глубже, паника во внешнем middleware его обойдёт и обрушит процесс. Оговорка: recover ловит только панику в той же горутине — паника в `go func` хендлера всё равно убьёт сервис. **В:** Почему CORS preflight (OPTIONS) надо обрабатывать до auth? **О:** Preflight-запрос браузер шлёт автоматически и **без** `Authorization`. Если auth стоит раньше, OPTIONS получит 401, браузер посчитает CORS-проверку проваленной и не отправит реальный запрос. CORS должен ответить на OPTIONS (обычно 204 с CORS-заголовками) и сделать short-circuit, не доходя до auth. **В:** Как корректно реализовать per-IP rate limiting на `golang.org/x/time/rate`? **О:** Держать `map[ip]*rate.Limiter` под мьютексом, создавать лимитер лениво на первый запрос с IP, вызывать `Allow()` (неблокирующий) и при `false` отдавать 429 с `Retry-After`. Обязательно фоновая очистка старых записей (по `lastSeen`), иначе утечка памяти. За прокси брать IP из доверенного `X-Forwarded-For`, не из `RemoteAddr`. Параметры: `rate.Limit` (rps) — скорость пополнения токенов, `burst` — ёмкость ведра (token bucket). **В:** Как передавать данные (например, user) из auth middleware в хендлер? **О:** Через `context.WithValue(r.Context(), key, val)` и `r.WithContext(ctx)`, где `key` — приватный пользовательский тип (не string), чтобы избежать коллизий. Хендлер достаёт через типобезопасный геттер с проверкой `ok`. Context — для request-scoped данных; нельзя складывать туда обязательные параметры функций или мутабельное общее состояние. **В:** Чем отличается порядок Logging снаружи Recovery от Recovery снаружи Logging? **О:** Если Recovery снаружи: при панике Logging уже мог отработать (или нет — зависит от того, где паника), а 500 отдаст Recovery; лог может зафиксировать неверный/нулевой статус. Если Logging снаружи: лог гарантированно выполнится для всех запросов, включая паникующие, но тогда Recovery вложен и не покроет панику самого Logging. На практике Recovery ставят внешним для гарантии 500, а статус для лога снимают через httpsnoop внутри Logging. **В:** Поддерживает ли `net/http` 1.22 middleware из коробки? **О:** Нет. 1.22 добавил в `ServeMux` методы (`GET /path`) и path-параметры (`r.PathValue`), но цепочек middleware в стандартном роутере нет. Глобальные middleware применяют, оборачивая весь `mux` (`Chain(...)(mux)`); для подмножеств — вручную оборачивают хендлеры или берут роутер вроде chi с `r.Use`/`r.Group`. ## На что копают на senior+ - **Глубокое понимание потери интерфейсов**: попросят объяснить через статический vs динамический тип, почему именно встраивание интерфейса не промотит Flush, и как httpsnoop динамически собирает правильную комбинацию интерфейсов (16 комбинаций Flusher/Hijacker/Pusher/ReaderFrom). - **Производительность цепочек**: каждое звено — это `http.HandlerFunc` (аллокация замыкания при сборке цепочки, но один раз на старте) и накладной вызов; обсуждение того, что цепочка собирается на старте, а не на каждый запрос, и что `r.WithContext` аллоцирует. - **Контекст и отмена**: связь middleware с `r.Context()`, дедлайнами, `context.WithTimeout` на уровне middleware, корректная отмена downstream-вызовов. - **Recovery и горутины**: ограничение recover одной горутиной, паттерны безопасного запуска фоновых горутин (отдельный recover внутри `go func`), `http.ErrAbortHandler`. - **Корректность Logging**: захват статуса при стриминге, проблема двойного `WriteHeader`, учёт размера тела, не ломать `io.ReaderFrom` (sendfile) обёрткой. - **Безопасность rate limiting**: подделка `X-Forwarded-For`, выбор distributed rate limit (Redis/token bucket в сторадже) vs локальный, fairness, тонкости token bucket vs leaky bucket vs sliding window. - **CORS-нюансы**: credentials + wildcard, `Vary: Origin`, кеширование preflight, `Access-Control-Expose-Headers`, обработка `Private Network Access` заголовков. - **Идемпотентность и иммутабельность**: почему `*http.Request` нужно обновлять через `WithContext` (shallow copy), а не мутировать поля; потокобезопасность shared-состояния в middleware. - **Тестирование**: как тестировать middleware изолированно через `httptest.NewRecorder` и фейковый `next`, проверка short-circuit, проверка проброса context. - **Альтернативные сигнатуры**: где оправдан `func(http.HandlerFunc) http.HandlerFunc`, паттерн с явным `next` и почему стандарт остаётся `func(http.Handler) http.Handler`.