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 - `http.Server` — это конфигурация сервера; `ListenAndServe` запускает accept-loop, на **каждое соединение создаётся отдельная горутина** (`conn.serve`). - `Handler` — интерфейс с единственным методом `ServeHTTP(w, r)`. `HandlerFunc` — адаптер, превращающий обычную функцию в `Handler`. - В **Go 1.22** `ServeMux` научился методам и wildcard-паттернам: `"GET /items/{id}"`, `{path...}`, доступ через `r.PathValue("id")`, есть детерминированный приоритет специфичности и паника при конфликтах. - **Таймауты — обязательны в проде.** Дефолтный `http.Server` без таймаутов уязвим к Slowloris (медленные клиенты держат соединения вечно). Минимум: `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout`, `IdleTimeout`. - `http.Client` и `http.Transport` потокобезопасны и **переиспользуют** TCP/TLS-соединения через пул. Создавать клиент на каждый запрос — антипаттерн (утечка соединений и портов). Всегда `defer resp.Body.Close()` и вычитывать тело до конца. - `r.Context()` отменяется при разрыве клиентского соединения и при истечении server-таймаутов. `context.WithValue` — только для request-scoped данных, не для DI и не для опциональных параметров. ## Теория ### http.Server: поля и accept loop `http.ListenAndServe(addr, handler)` — это удобная обёртка, которая создаёт `&http.Server{Addr: addr, Handler: handler}` **без единого таймаута**. В проде так делать нельзя — нужно конфигурировать `http.Server` явно. ```go srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20, // 1 MiB на заголовки ErrorLog: log.New(os.Stderr, "http: ", log.LstdFlags), BaseContext: func(net.Listener) context.Context { return rootCtx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return ctx }, } log.Fatal(srv.ListenAndServe()) ``` Ключевые поля: | Поле | Назначение | |------|-----------| | `Addr` | адрес `host:port` | | `Handler` | корневой `Handler`; если `nil` — используется `http.DefaultServeMux` | | `ReadTimeout` / `ReadHeaderTimeout` / `WriteTimeout` / `IdleTimeout` | таймауты (см. ниже) | | `MaxHeaderBytes` | лимит на размер заголовков (по умолчанию 1 MiB) | | `TLSConfig` | конфигурация TLS для `ListenAndServeTLS` | | `BaseContext` | базовый контекст для всех входящих запросов | | `ConnContext` | модификация контекста per-connection | | `ConnState` | хук на смену состояния соединения (New/Active/Idle/Closed) | | `ErrorLog` | логгер ошибок сервера | **Как работает accept loop.** `ListenAndServe` вызывает `net.Listen("tcp", addr)`, затем `srv.Serve(ln)`. В `Serve` крутится бесконечный цикл: ```go for { rw, err := l.Accept() // блокирующий accept нового соединения if err != nil { /* backoff при временной ошибке, иначе return */ } c := srv.newConn(rw) go c.serve(connCtx) // ОТДЕЛЬНАЯ горутина на каждое соединение } ``` Важные следствия модели "горутина на соединение": - Нет фиксированного пула воркеров — число горутин растёт линейно с числом активных соединений. При 100k keep-alive соединений — 100k горутин (это ок для Go, но память на стеки и планировщик не бесплатны). - Внутри одной горутины соединения HTTP-запросы обрабатываются последовательно (для HTTP/1.1 keep-alive). Для HTTP/2 один TCP-conn мультиплексирует много стримов, и под каждый стрим тоже выделяется горутина. - Паника в `ServeHTTP` ловится `conn.serve` через `recover`, логируется и закрывает соединение — она **не роняет сервер**, но и не превращается в 500 автоматически (тело может быть уже частично записано). Свой recover-middleware всё равно нужен. **Graceful shutdown:** ```go ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("graceful shutdown failed: %v", err) _ = srv.Close() // жёсткое закрытие } ``` `Shutdown` перестаёт принимать новые соединения, закрывает idle-соединения и ждёт завершения активных запросов (до дедлайна ctx). `Close` рвёт всё немедленно. `ListenAndServe` при штатном shutdown возвращает `http.ErrServerClosed` — это не ошибка. ### Handler, HandlerFunc, Handle/HandleFunc ```go type Handler interface { ServeHTTP(ResponseWriter, *Request) } ``` `HandlerFunc` — адаптер: тип-функция, у которого метод `ServeHTTP` просто вызывает саму функцию. Это классический пример "function-as-interface": ```go type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ``` Поэтому любую `func(w, r)` можно привести: `http.HandlerFunc(myFunc)` — и она станет `Handler`. - `http.Handle(pattern, handler)` — регистрирует `Handler` в `DefaultServeMux`. - `http.HandleFunc(pattern, fn)` — то же, но принимает функцию (внутри оборачивает в `HandlerFunc`). Middleware строится как функция `func(http.Handler) http.Handler`: ```go func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) }) } ``` ### ServeMux: классический и роутинг Go 1.22 **Классический (до 1.22).** Паттерны без методов. Различались два вида: - Точный путь: `"/items"` — совпадает только с `/items`. - Subtree (с завершающим `/`): `"/items/"` — совпадает с `/items/` и всем, что под ним. Также делает редирект `/items` → `/items/` (301). Выбирается **самый длинный** совпавший паттерн. Метода и path-параметров нет — приходилось парсить путь вручную или брать сторонний роутер (chi, gorilla/mux, httprouter). **Go 1.22: enhanced routing.** Паттерн теперь может содержать метод и wildcards. ```go mux := http.NewServeMux() // Метод в паттерне mux.HandleFunc("GET /items/{id}", getItem) mux.HandleFunc("POST /items", createItem) mux.HandleFunc("DELETE /items/{id}", deleteItem) // Wildcard на остаток пути (только в конце) mux.HandleFunc("GET /files/{path...}", serveFile) // Хост в паттерне mux.HandleFunc("api.example.com/health", health) func getItem(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // извлечение значения wildcard path := r.PathValue("path") // для {path...} — весь хвост _ = id; _ = path } ``` Правила паттернов: | Конструкция | Смысл | |-------------|-------| | `GET /x` | метод GET; `GET` также неявно покрывает `HEAD` | | `/x` (без метода) | любой метод | | `{id}` | один сегмент пути; доступен через `r.PathValue("id")` | | `{path...}` | многосегментный остаток (только в самом конце паттерна) | | `/x/{$}` | конец пути — совпадает только с `/x/`, но не с `/x/y` | | `{_}` | анонимный wildcard (совпадает, но имя не сохраняется) | | `example.com/x` | привязка к хосту | **Приоритет паттернов (специфичность).** Если запрос подходит под несколько паттернов, выигрывает **более специфичный**, а не более длинный. Формально: паттерн P1 специфичнее P2, если P1 совпадает со строгим подмножеством запросов P2. Литеральный сегмент специфичнее wildcard, а `{id}` специфичнее `{path...}`. ```go mux.HandleFunc("GET /items/{id}", byID) // специфичнее mux.HandleFunc("GET /items/latest", latest) // ещё специфичнее (литерал) // Запрос GET /items/latest -> latest (литерал бьёт wildcard) // Запрос GET /items/42 -> byID ``` **Конфликты паттернов.** Если два паттерна совпадают с одним и тем же множеством запросов и ни один не специфичнее другого, `ServeMux` **паникует при регистрации** (а не молча выбирает один). Пример конфликта: ```go mux.HandleFunc("GET /a/{x}/b", h1) mux.HandleFunc("GET /a/c/{y}", h2) // /a/c/b подходит под оба, и ни один не специфичнее -> panic при HandleFunc ``` Прочие особенности 1.22: - При несовпадении метода (путь есть, метод другой) `ServeMux` отдаёт **405 Method Not Allowed** и заполняет заголовок `Allow`. - `ServeMux` теперь **очищает и нормализует** путь (`..`, `//`), делая редирект на канонический URL. - Совместимость: можно отключить новое поведение через `GODEBUG=httpmuxgo121=1`. Wildcards не "съедают" `/`: `{id}` — ровно один сегмент. Для жадного захвата используйте `{rest...}`. ### Таймауты сервера (критично!) **Почему дефолт опасен — Slowloris.** Без таймаутов злоумышленник (или просто кривой/медленный клиент) открывает соединение и шлёт заголовки по одному байту в секунду, никогда не завершая запрос. Соединение и его горутина живут бесконечно. Несколько тысяч таких соединений исчерпывают файловые дескрипторы / память — отказ в обслуживании без всякого флуда трафиком. `http.ListenAndServe` и `http.Server{}` без таймаутов уязвимы по умолчанию. | Таймаут | Что покрывает | На что влияет, риск без него | |---------|---------------|------------------------------| | `ReadHeaderTimeout` | от accept до конца чтения заголовков запроса | прямая защита от Slowloris на заголовках; самый дешёвый и безопасный таймаут | | `ReadTimeout` | от accept до конца чтения **всего** тела запроса | защита от медленной отправки тела; но обрезает легитимные большие/долгие аплоады | | `WriteTimeout` | от конца чтения заголовков до конца записи ответа | защита от медленных читателей; обрезает долгие ответы/стриминг | | `IdleTimeout` | сколько keep-alive соединение живёт между запросами | без него idle-соединения копятся; если 0 — используется `ReadTimeout` | | `MaxHeaderBytes` | лимит размера заголовков | защита от раздувания памяти заголовками | Тонкости: - `ReadTimeout` и `WriteTimeout` — это дедлайны на уровне `net.Conn` (`SetReadDeadline`/`SetWriteDeadline`), они **не знают про логику хендлера**. `WriteTimeout` отсчитывается грубо с момента чтения заголовков, поэтому он ограничивает суммарное время обработки + записи. Для долгих стримов (SSE, скачивание больших файлов) `WriteTimeout` нужно ставить большим или 0, а защищать иначе. - Для гранулярного per-handler таймаута есть **`http.TimeoutHandler`**: ```go h := http.TimeoutHandler(slowHandler, 2*time.Second, "request timed out") ``` `TimeoutHandler` запускает хендлер в отдельной горутине, и если тот не уложился — отдаёт **503** с заданным сообщением. Важно: он **буферизует** ответ (использует свой ResponseWriter), поэтому не работает со стримингом / `Flusher` / `Hijacker`, и хендлер-горутина может продолжать жить после таймаута (контекст запроса при этом отменяется). - Рекомендуемый "безопасный по умолчанию" минимум: всегда задавать `ReadHeaderTimeout` (даже если остальное специфично для эндпоинтов), затем `IdleTimeout`, затем `Read/WriteTimeout` под профиль нагрузки. ### http.Client, Transport и пул соединений ```go var ErrClientCannotBeRecreated = errors.New("don't do this") // ПЛОХО: новый клиент (а значит и новый Transport, и новый пул) на каждый вызов func badFetch(url string) (*http.Response, error) { return (&http.Client{}).Get(url) // соединения не переиспользуются между вызовами } // ХОРОШО: один долгоживущий клиент на весь процесс/зависимость var httpClient = &http.Client{ Timeout: 10 * time.Second, // общий дедлайн на весь запрос Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, // ВАЖНО: дефолт всего 2! MaxConnsPerHost: 0, // 0 = без лимита IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 5 * time.Second, ExpectContinueTimeout: 1 * time.Second, ResponseHeaderTimeout: 5 * time.Second, ForceAttemptHTTP2: true, }, } ``` **Почему нельзя создавать клиент на каждый запрос.** Пул keep-alive соединений живёт внутри `http.Transport`. Новый клиент = новый `Transport` = пустой пул: каждый запрос делает новый TCP+TLS handshake (дорого), а старые соединения уходят в `TIME_WAIT`. Под нагрузкой это приводит к **исчерпанию эфемерных портов** и деградации латентности. `http.Client` и `http.Transport` потокобезопасны — один экземпляр обслуживает любое число параллельных горутин. Параметры пула: | Параметр | Значение | Дефолт | |----------|----------|--------| | `MaxIdleConns` | максимум idle-соединений во всём пуле | 100 | | `MaxIdleConnsPerHost` | максимум idle-соединений на один хост | **2** (частая причина деградации!) | | `MaxConnsPerHost` | жёсткий лимит активных+idle на хост (блокирует/ждёт при превышении) | 0 (без лимита) | | `IdleConnTimeout` | сколько idle-соединение живёт до закрытия | 90s | Если приложение ходит интенсивно к одному upstream, дефолтный `MaxIdleConnsPerHost = 2` означает, что при >2 параллельных запросах лишние соединения после ответа **сразу закрываются** и не переиспользуются — поднимайте этот лимит. **Обязательно закрывать `resp.Body` и вычитывать его.** ```go resp, err := httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() // Чтобы соединение вернулось в пул, тело нужно дочитать ДО конца: _, _ = io.Copy(io.Discard, resp.Body) ``` Если не закрыть `Body` — утечка соединения и горутины (соединение никогда не вернётся в пул). Если закрыть, но **не дочитать** тело до EOF, Transport не сможет переиспользовать соединение и закроет его (для маленьких ответов это норм, но при потоке запросов теряется весь смысл keep-alive). Поэтому идиома: `defer Close()` + `io.Copy(io.Discard, body)` после обработки. **Таймауты клиента и контекст.** - `http.Client.Timeout` — общий дедлайн на весь запрос: соединение, отправка, ожидание заголовков, **чтение всего тела**. Если тело читается долго, Timeout оборвёт чтение в середине. Поэтому для стриминга `Timeout` неудобен. - Гранулярные таймауты — в `Transport`: `TLSHandshakeTimeout`, `ResponseHeaderTimeout` (до получения заголовков ответа, не считая чтение тела), `ExpectContinueTimeout`. - **Отмена через контекст** — предпочтительный способ для per-request дедлайнов и отмены: ```go ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) resp, err := httpClient.Do(req) // прервётся при отмене ctx или дедлайне ``` Контекст отменяет и in-flight запрос (закрывает соединение), и чтение тела. ### Контекст запроса (сервер) `r.Context()` возвращает контекст входящего запроса. Он: - **Отменяется**, когда клиент рвёт соединение (и при HTTP/2 reset стрима), а также при `Server.Shutdown`. Это позволяет хендлеру и нисходящим вызовам (БД, upstream) прекратить бесполезную работу: ```go func handler(w http.ResponseWriter, r *http.Request) { rows, err := db.QueryContext(r.Context(), "SELECT ...") // отменится при разрыве ... } ``` - Несёт **request-scoped значения** через middleware (request ID, аутентифицированный пользователь, трейс): ```go type ctxKey int const userKey ctxKey = 0 func auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u := authenticate(r) ctx := context.WithValue(r.Context(), userKey, u) next.ServeHTTP(w, r.WithContext(ctx)) // r.WithContext возвращает копию }) } func userFromCtx(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userKey).(*User) return u, ok } ``` **Антипаттерны `context.WithValue`:** - Ключ типа `string`/встроенного типа — риск коллизий. Используйте **неэкспортируемый тип ключа** (`type ctxKey int`). - Прятать в контекст обязательные зависимости (логгер, конфиг, сервисы) вместо явной передачи через параметры/структуру — это скрытый DI, который ломает читаемость и типобезопасность. - Класть в контекст параметры функции, которые могли бы быть аргументами. Контекст — для request-scoped метаданных, проходящих сквозь границы API, а не для "удобного глобального мешка". - Изменяемые значения в контексте: контекст иммутабелен по дизайну, мутируемое состояние через него — источник гонок. ### ResponseWriter: порядок записи, Flusher, Hijacker ```go type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int) } ``` Правила: - **Заголовки задаются до тела.** Менять `w.Header()` нужно **до** первого `Write` или `WriteHeader`. Первый `Write` неявно вызывает `WriteHeader(200)`, после чего изменения заголовков уже отправлены и игнорируются (Go залогирует `http: superfluous response.WriteHeader call` при повторном вызове). ```go w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) // статус ДО тела _ = json.NewEncoder(w).Encode(payload) // w.Header().Set(...) здесь УЖЕ бесполезно ``` - `WriteHeader` можно вызвать только один раз; повторный — no-op с логом. - **`http.Flusher`** — для стриминга (SSE, chunked). Позволяет вытолкнуть буфер клиенту, не дожидаясь конца ответа: ```go func sse(w http.ResponseWriter, r *http.Request) { fl, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming unsupported", 500); return } w.Header().Set("Content-Type", "text/event-stream") for { select { case <-r.Context().Done(): return case ev := <-events: fmt.Fprintf(w, "data: %s\n\n", ev) fl.Flush() // немедленно отправить клиенту } } } ``` - **`http.Hijacker`** — забирает сырое TCP-соединение (для WebSocket и т.п.). После `Hijack()` сервер **перестаёт** управлять соединением: нельзя больше использовать `ResponseWriter`, писать через него или вызывать `WriteHeader` — вы отвечаете за чтение/запись и закрытие `net.Conn` сами. ```go hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "hijack unsupported", 500); return } conn, bufrw, err := hj.Hijack() if err != nil { return } defer conn.Close() // дальше — только conn/bufrw; w использовать НЕЛЬЗЯ ``` - Тип, реализующий `ResponseWriter`, опционально реализует `Flusher`/`Hijacker`/`Pusher` — проверяйте type-assertion. Обёртки-middleware над `ResponseWriter` (например, для подсчёта статуса) **теряют** эти интерфейсы, если их не прокинуть явно — частый баг, ломающий стриминг/WebSocket. ## Подводные камни / gotchas - **`http.ListenAndServe` без таймаутов** — уязвимость Slowloris. Никогда не использовать в проде; всегда конфигурировать `http.Server` с таймаутами. - **`MaxIdleConnsPerHost = 2` по умолчанию** — при интенсивных запросах к одному хосту соединения не переиспользуются. Тихая деградация производительности. - **Создание `http.Client`/`Transport` на каждый запрос** — пустой пул, лишние handshakes, исчерпание портов (`TIME_WAIT`). - **Незакрытый `resp.Body`** — утечка соединения и горутины. **Закрытый, но недочитанный** `Body` — соединение не возвращается в пул. - **`WriteTimeout` обрезает долгие ответы/стриминг** — для SSE и больших скачиваний ставьте больше/0 и защищайтесь иначе. - **`http.TimeoutHandler` несовместим со стримингом** (буферизует ответ, прячет `Flusher`/`Hijacker`); хендлер-горутина может пережить таймаут. - **Изменение заголовков после первого `Write`** не имеет эффекта (`superfluous WriteHeader`). - **Обёртка над `ResponseWriter` теряет `Flusher`/`Hijacker`** — ломает стриминг и WebSocket, если интерфейсы не прокинуты. - **Конфликт паттернов в ServeMux 1.22 — паника при старте** (а не молчаливый выбор). Это фича, но ловит врасплох. - **Wildcard `{id}` — ровно один сегмент**, не жадный; для остатка пути нужен `{path...}`. - **Паника в хендлере** ловится сервером, но не превращается в 500 автоматически и может оставить полузаписанный ответ — нужен свой recover-middleware. - **`context.WithValue` со строковым ключом** — риск коллизий; используйте приватный тип ключа. ## Вопросы на собеседовании **В:** Какая конкурентная модель у `net/http` сервера? **О:** Accept-loop в `Server.Serve` принимает соединения и запускает по горутине на каждое соединение (`go c.serve`). HTTP/1.1 keep-alive: запросы в одном соединении обрабатываются последовательно той же горутиной. HTTP/2: один TCP-conn мультиплексирует стримы, под каждый стрим — своя горутина. Фиксированного пула воркеров нет, число горутин растёт с числом соединений. **В:** Почему дефолтный `http.Server` опасен в проде и что такое Slowloris? **О:** Без таймаутов медленный/злонамеренный клиент держит соединение открытым бесконечно (шлёт заголовки/тело по байту), занимая дескриптор и горутину. Тысячи таких соединений → отказ в обслуживании без флуда трафиком. Лечится `ReadHeaderTimeout` (минимум), плюс `ReadTimeout`, `WriteTimeout`, `IdleTimeout`, `MaxHeaderBytes`. **В:** Чем отличаются `ReadTimeout`, `ReadHeaderTimeout`, `WriteTimeout`, `IdleTimeout`? **О:** `ReadHeaderTimeout` — дедлайн на чтение заголовков (от accept). `ReadTimeout` — на чтение всего запроса включая тело. `WriteTimeout` — от конца чтения заголовков до конца записи ответа (фактически ограничивает обработку+запись). `IdleTimeout` — время жизни keep-alive соединения между запросами; если 0, берётся `ReadTimeout`. Это net-deadline'ы, они не знают логики хендлера; для per-handler — `http.TimeoutHandler` или контекст. **В:** Что появилось в роутинге `ServeMux` в Go 1.22? **О:** Методы в паттернах (`"GET /items/{id}"`), wildcards `{id}` (один сегмент) и `{path...}` (остаток), `r.PathValue()`, `{$}` для точного конца, привязка к хосту. Приоритет — по специфичности (литерал > wildcard), а не по длине. Конфликтующие паттерны вызывают панику при регистрации. Несовпадение метода → 405 с заголовком `Allow`. Путь нормализуется с редиректом на канонический. **В:** Как определяется победитель при пересечении паттернов и что такое конфликт? **О:** Побеждает более специфичный паттерн — тот, чьё множество совпадающих запросов является строгим подмножеством другого (литерал специфичнее wildcard). Если ни один не специфичнее и множества пересекаются неоднозначно — это конфликт, и `ServeMux` паникует при `Handle`/`HandleFunc`. **В:** Почему нельзя создавать `http.Client` на каждый запрос? **О:** Пул keep-alive соединений живёт в `Transport`. Новый клиент → новый Transport → пустой пул: каждый раз TCP+TLS handshake, старые соединения уходят в `TIME_WAIT`, исчерпываются эфемерные порты. `Client`/`Transport` потокобезопасны — нужно держать один на процесс/зависимость и тюнить `MaxIdleConnsPerHost` (дефолт 2). **В:** Почему обязательно закрывать и дочитывать `resp.Body`? **О:** `Close()` возвращает соединение в пул и освобождает ресурсы; без него — утечка соединения/горутины. Но если тело не дочитано до EOF, Transport не может переиспользовать соединение и закроет его. Идиома: `defer resp.Body.Close()` плюс `io.Copy(io.Discard, resp.Body)`. **В:** Как правильно отменять исходящий HTTP-запрос по таймауту? **О:** Предпочтительно через контекст: `http.NewRequestWithContext` + `context.WithTimeout`. Это отменяет и установление соединения, и ожидание ответа, и чтение тела. `Client.Timeout` — грубый общий дедлайн на весь запрос (мешает стримингу). Гранулярные `TLSHandshakeTimeout`/`ResponseHeaderTimeout` — в Transport. **В:** Когда отменяется `r.Context()` и зачем его прокидывать вниз? **О:** Отменяется при разрыве клиентского соединения, reset HTTP/2-стрима и при `Server.Shutdown`. Прокидывание в `QueryContext`/исходящие запросы даёт раннюю отмену бесполезной работы и освобождение ресурсов, когда клиент уже ушёл. **В:** Какие правила и подводные камни у `ResponseWriter`? **О:** Заголовки — только до первого `Write`/`WriteHeader` (первый `Write` неявно шлёт 200). `WriteHeader` — один раз. Для стриминга нужен `http.Flusher`. После `Hijack()` `ResponseWriter` использовать нельзя — соединение ваше. Обёртки над `ResponseWriter` теряют `Flusher`/`Hijacker`, если их не прокинуть. **В:** Какие антипаттерны у `context.WithValue`? **О:** Строковый/встроенный тип ключа (коллизии) — нужен приватный тип. Передача обязательных зависимостей (логгер, сервисы, конфиг) — это скрытый DI, ломающий типобезопасность; такие вещи передают явно. Контекст — только для request-scoped метаданных, пересекающих границы API, и он иммутабелен. ## На что копают на senior+ - Точная семантика `WriteTimeout` относительно момента чтения заголовков и почему он не годится для долгих стримов; альтернативы (`TimeoutHandler`, контекстные дедлайны, ручные `SetWriteDeadline` через `http.ResponseController` в Go 1.20+). - `http.ResponseController` (Go 1.20+) для per-request управления дедлайнами и `Flush` поверх обёрток ResponseWriter. - Поведение пула при HTTP/2 (мультиплексирование, отдельный путь, `ForceAttemptHTTP2`) и почему `MaxConnsPerHost`/`MaxIdleConnsPerHost` ведут себя иначе. - Что именно происходит с горутиной хендлера после срабатывания `TimeoutHandler` или отмены контекста (она не убивается принудительно — нужно самому реагировать на `ctx.Done()`). - Внутреннее устройство accept-loop: backoff при временных `Accept`-ошибках, `ConnState`/`BaseContext`/`ConnContext` хуки. - Корректный graceful shutdown с дренажом, обработка `ErrServerClosed`, координация с балансировщиком (readiness-проба). - Алгоритм специфичности паттернов 1.22 и детерминированность конфликтов; как мигрировать со стороннего роутера и где `net/http` всё ещё уступает (отсутствие группировки роутов, middleware-цепочек из коробки). - Утечки портов/`TIME_WAIT` под нагрузкой, настройка `MaxIdleConnsPerHost`, влияние недочитанного тела на переиспользование соединений. - Безопасная обёртка `ResponseWriter` с сохранением опциональных интерфейсов (Flusher/Hijacker/Pusher) или через `ResponseController`.