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

HTTP-роутеры и middleware

Зачем знать: HTTP — основная транспорт-обвязка для backend на Go. От выбора роутера зависит, как пишутся handlers, middleware и какие ловушки ждут на проде. С Go 1.22 stdlib net/http стал «достаточно мощным», что заметно изменило экосистему. На middle-уровне ждут понимания — когда хватает stdlib, когда брать chi/gin/echo, и КАК работают middleware.

  1. Базовая концепция
  2. В Go идиоматично
  3. Gotchas
  4. Best practices
  5. Вопросы на собесе
  6. Practice
  7. Источники

net/http — стандартная библиотека Go для HTTP. Содержит:

  • http.Server — сам сервер;
  • http.Handler — интерфейс с ServeHTTP(w, r);
  • http.ServeMux — встроенный роутер;
  • http.Client — HTTP-клиент;
  • middleware-style декораторы.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

Любой объект, реализующий ServeHTTP — handler. Любая функция с такой сигнатурой — через http.HandlerFunc.

До Go 1.22 встроенный mux был примитивен (только префиксы, без method matching). С 1.22 он поддерживает:

  • Method matching: mux.HandleFunc("GET /users", handler)
  • Path parameters: mux.HandleFunc("GET /users/{id}", handler)r.PathValue("id")
  • Wildcards: mux.HandleFunc("GET /files/{path...}", handler) — захватывает остаток пути.
  • Host matching: mux.HandleFunc("api.example.com/", handler)
  • Precedence rules: более специфичный путь имеет приоритет.
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "user %s", id)
})
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /files/{path...}", serveFile)

Для большинства сервисов этого хватает. Раньше нужно было идти за chi/gin.

  • Совместим с net/http (http.Handler).
  • Sub-routers (вложенные группы).
  • Middleware chain.
  • Wildcard, regex routing.
  • chi.URLParam(r, "id") для path params.
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
fmt.Fprintf(w, "user %s", id)
})
r.Route("/admin", func(r chi.Router) {
r.Use(adminAuth)
r.Get("/users", listAllUsers)
})

Совместим с любым http.Handler middleware. Самый идиоматичный выбор для Go.

  • НЕ реализует http.Handler. Свой gin.Context.
  • Быстрый (radix tree).
  • Огромное community, много примеров.
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})

Минусы:

  • Не совместим с middleware из net/http экосистемы;
  • Свой DSL для роутинга, JSON-respnose, и т.п.;
  • В сложных случаях — vendor lock.
  • Похож на gin, тоже свой Context.
  • Group middleware.
  • Validator, renderer интегрированы.
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]string{"id": id})
})
  • Основан на fasthttp (НЕ net/http!).
  • Невероятно быстрый для simple workloads.
  • НЕ совместим с net/http middleware.
  • НЕ поддерживает HTTP/2.
import "github.com/gofiber/fiber/v2"
app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
return c.SendString("user " + c.Params("id"))
})

⚠️ fiber — отдельная экосистема, не совместима с net/http. Это и плюс (скорость), и минус (lock-in). Многие production-команды отказались из-за отсутствия HTTP/2 и SSE-проблем.

Параметрnet/http (1.22+)chiginechofiber
Совместимость с net/http
Performanceхорошийхорошийочень хорошийочень хорошиймаксимальный
HTTP/2
Middleware ecosystemnet/httpnet/httpginechofiber
Learning curveнизкаянизкаясредняясредняясредняя
Когда выбиратьдефолтдефолт для роутераесли нужен gin DSLальтернатива ginмаксимум RPS, нет HTTP/2

Рекомендация для middle 1: net/http + chi (если нужно больше, чем stdlib даёт). Это покрывает 90% задач.


package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // защита от Slowloris
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
}

⚠️ Обязательно ставьте таймауты. Без ReadHeaderTimeout сервер уязвим к Slowloris-атакам.

package main
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// встроенные middleware
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Route("/api/v1", func(r chi.Router) {
r.Use(authMiddleware)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Use(userCtx)
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
})
http.ListenAndServe(":8080", r)
}

Middleware — это функция, оборачивающая http.Handler.

func LoggingMiddleware(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Применение
handler := LoggingMiddleware(AuthMiddleware(MyHandler))

Без либы — вручную:

type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
handler := Chain(myHandler,
LoggingMiddleware,
AuthMiddleware,
RateLimitMiddleware,
)
// Порядок: Logging → Auth → RateLimit → MyHandler

С chi — r.Use(...):

r := chi.NewRouter()
r.Use(LoggingMiddleware)
r.Use(AuthMiddleware)
r.Use(RateLimitMiddleware)
// Тот же порядок
r := chi.NewRouter()
r.Use(LoggingMiddleware) // global
r.Get("/public", publicHandler) // только Logging
r.Group(func(r chi.Router) {
r.Use(AuthMiddleware)
r.Get("/private", privateHandler) // Logging + Auth
})
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
type ctxKey int
const requestIDKey ctxKey = 1
func GetRequestID(ctx context.Context) string {
if v, ok := ctx.Value(requestIDKey).(string); ok {
return v
}
return ""
}
func Recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rvr := recover(); rvr != nil {
if rvr == http.ErrAbortHandler {
panic(rvr)
}
log.Printf("panic: %v\n%s", rvr, debug.Stack())
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}

⚠️ Важно: http.ErrAbortHandler нужно «прокидывать дальше», это сигнал отмены, а не реальная паника.

func Timeout(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

⚠️ Это только добавляет дедлайн в context. Чтобы handler реально прервался, он должен проверять ctx.Done() или передавать ctx в downstream-вызовы. Иначе timeout middleware ничего не остановит.

func CORS(allowOrigins []string) func(http.Handler) http.Handler {
allow := make(map[string]bool, len(allowOrigins))
for _, o := range allowOrigins {
allow[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 allow[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Request-ID")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}

В production используйте rs/cors или встроенный chi cors.Handler — там больше edge cases.

import "github.com/golang-jwt/jwt/v5"
func JWTAuth(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHdr := r.Header.Get("Authorization")
if !strings.HasPrefix(authHdr, "Bearer ") {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
tokenStr := strings.TrimPrefix(authHdr, "Bearer ")
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected method: %v", t.Header["alg"])
}
return secret, nil
})
if err != nil || !token.Valid {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
claims := token.Claims.(jwt.MapClaims)
ctx := context.WithValue(r.Context(), userIDKey, claims["sub"])
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
import "golang.org/x/time/rate"
func RateLimit(rps float64, burst int) func(http.Handler) http.Handler {
type clientLimiter struct {
limiter *rate.Limiter
last time.Time
}
var (
mu sync.Mutex
clients = make(map[string]*clientLimiter)
)
// GC устаревших клиентов
go func() {
for {
time.Sleep(time.Minute)
mu.Lock()
for ip, c := range clients {
if time.Since(c.last) > 5*time.Minute {
delete(clients, ip)
}
}
mu.Unlock()
}
}()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getClientIP(r)
mu.Lock()
c, ok := clients[ip]
if !ok {
c = &clientLimiter{limiter: rate.NewLimiter(rate.Limit(rps), burst)}
clients[ip] = c
}
c.last = time.Now()
mu.Unlock()
if !c.limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}

В production для распределённого rate limiting — Redis + token bucket.

import "github.com/prometheus/client_golang/prometheus/promauto"
var (
httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
}, []string{"method", "path", "status"})
httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
Buckets: prometheus.DefBuckets,
}, []string{"method", "path"})
)
func Metrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// нужен wrapper, чтобы поймать статус
ww := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
path := chi.RouteContext(r.Context()).RoutePattern() // НЕ raw path!
httpRequestsTotal.WithLabelValues(r.Method, path, strconv.Itoa(ww.status)).Inc()
httpDuration.WithLabelValues(r.Method, path).Observe(time.Since(start).Seconds())
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}

⚠️ Используйте route pattern (/users/{id}), а не r.URL.Path (/users/123), иначе кардинальность метрик взорвётся.

import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=18,lte=120"`
}
var validate = validator.New(validator.WithRequiredStructEnabled())
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if err := validate.Struct(req); err != nil {
// вернуть детали ошибки
var ve validator.ValidationErrors
if errors.As(err, &ve) {
details := make(map[string]string)
for _, fe := range ve {
details[fe.Field()] = fe.Tag()
}
// ... возвращаем 400 с details
}
http.Error(w, "validation error", http.StatusBadRequest)
return
}
// ...
}
validate.RegisterValidation("uuid_v4", func(fl validator.FieldLevel) bool {
_, err := uuid.Parse(fl.Field().String())
return err == nil
})
type Req struct {
ID string `validate:"uuid_v4"`
}

Пишете комментарии — генерируете spec.

// @Summary Create user
// @Description Create new user
// @Accept json
// @Produce json
// @Param request body CreateUserRequest true "request body"
// @Success 201 {object} User
// @Failure 400 {object} ErrorResponse
// @Router /users [post]
func createUser(w http.ResponseWriter, r *http.Request) { ... }
Окно терминала
$ swag init -g cmd/api/main.go
# создаёт docs/swagger.yaml

Pros: легко начать. Cons: spec — production артефакт, тяжело держать в sync с реальным API.

Сначала пишете OpenAPI spec (yaml), потом генерируете handlers.

api/openapi.yaml
paths:
/users:
post:
operationId: createUser
requestBody: ...
Окно терминала
$ oapi-codegen -package api api/openapi.yaml > internal/api/server.go

Генерируется интерфейс, который вы реализуете:

type ServerInterface interface {
CreateUser(w http.ResponseWriter, r *http.Request)
GetUser(w http.ResponseWriter, r *http.Request, id string)
}
type Server struct{}
func (s *Server) CreateUser(w http.ResponseWriter, r *http.Request) {
// ваша имплементация
}

Pros: contract-first, spec — source of truth. Cons: сложнее в начале.

Production рекомендация: spec-first для API, важных для интеграции. Code-first — для внутренних или прототипов.

Однонаправленный поток server → client поверх HTTP.

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 unsupported", http.StatusInternalServerError)
return
}
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case t := <-ticker.C:
fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
flusher.Flush()
}
}
}

⚠️ Не забудьте Flusher. Без Flush() данные буферизуются и не уходят клиенту.

Самая популярная либа — gorilla/websocket. С 2022 года её также активно поддерживает сообщество (gorilla/websocket maintenance возобновлён).

import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // настройте по необходимости
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
if err := conn.WriteMessage(msgType, msg); err != nil {
log.Println("write:", err)
break
}
}
}

Альтернатива — coder/websocket (бывшая nhooyr.io/websocket) — современная, без external deps.

Был добавлен в Go 1.8, но deprecated в Chrome (с 2022) и большинстве других браузеров. Не используйте в новых проектах.

Если действительно нужно (gRPC к примеру использует HTTP/2 multiplexing, что НЕ то же самое что server push):

func handler(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
pusher.Push("/static/app.css", nil)
}
// ...
}

// ПЛОХО:
srv := &http.Server{Addr: ":8080", Handler: mux}
// нет таймаутов → уязвимость Slowloris (атакующий шлёт по 1 байту в секунду)

Всегда ставьте ReadHeaderTimeout, минимум.

func Timeout(d time.Duration) func(http.Handler) http.Handler {
// ставит ctx с deadline, но если handler не проверяет ctx — не помогает
}

Timeout(5s) НЕ убьёт handler, который делает time.Sleep(10s). Чтобы это работало, handler должен:

  • Передавать r.Context() в downstream (db, http client);
  • Сам проверять <-ctx.Done() в long-running loops.
// ПЛОХО:
w.WriteHeader(http.StatusOK)
// ... ошибка ...
http.Error(w, "err", http.StatusInternalServerError)
// уже WriteHeader(200) — заголовок не поменяется, в логах warning

Нельзя писать заголовок дважды. Если уже начали response — придётся писать ошибку в тело.

var counters = make(map[string]int)
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
counters[r.URL.Path]++ // RACE!
next.ServeHTTP(w, r)
})
}

HTTP server параллелит handlers по умолчанию. Защищайте map mutex’ом или используйте sync.Map, лучше — atomic-counter или Prometheus metric.

resp, _ := http.Get("...")
data, _ := io.ReadAll(resp.Body)
// забыли resp.Body.Close() → leak соединений

Всегда defer resp.Body.Close() (для клиента) или обработать errors Decode (на сервере defer не нужен).

json.NewDecoder(r.Body).Decode(&req)
// если в body больше данных, чем JSON — connection не reusable

Если хотите reuse keep-alive connection — нужно дочитать body. Обычно io.Copy(io.Discard, r.Body) или defer r.Body.Close().

// ПЛОХО: высокая кардинальность Prometheus метрик
counter.WithLabelValues(r.URL.Path).Inc()
// /users/123, /users/124, /users/125 → 3 разных series

Используйте route pattern:

counter.WithLabelValues(chi.RouteContext(r.Context()).RoutePattern()).Inc()
// /users/{id} → 1 series
// ПЛОХО:
func handler(...) {
v := validator.New() // создание на каждый request — медленно
v.Struct(req)
}
// ХОРОШО:
var validate = validator.New()
func handler(...) {
validate.Struct(req)
}

validator дорого создать. Делайте один глобальный.

r.Route("/api", func(r chi.Router) {
r.Use(authMiddleware) // НЕ применится, если использовали Mount
r.Get("/users", h)
})
// VS
sub := chi.NewRouter()
sub.Get("/users", h)
r.Mount("/api", sub) // middleware на sub не применится!

Используйте Route() для inline-определения, Mount() — для готового sub-router (нужно вешать middleware на него отдельно).

// gin Handler
r.Use(func(c *gin.Context) { ... })
// net/http middleware
r.Use(func(next http.Handler) http.Handler { ... }) // НЕ работает в gin

Это причина, по которой переход с gin/echo на net/http (или наоборот) дорогой — все middleware придётся переписывать.

err := srv.ListenAndServe()
// после Shutdown() возвращает http.ErrServerClosed

http.ErrServerClosed — нормально, это не ошибка. Игнорируйте:

if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
// {"foo": 1, "bar": 2} → req заполнится zero-values, без ошибки

Для strict parsing:

dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil { ... }

3.13 ResponseWriter не безопасен после возврата из Handler

Заголовок раздела «3.13 ResponseWriter не безопасен после возврата из Handler»
func handler(w http.ResponseWriter, r *http.Request) {
go func() {
w.Write([]byte("...")) // НЕЛЬЗЯ! Handler уже завершён.
}()
}

После выхода из handler w уже невалиден. Если нужна async-работа — пишите в response до возврата, либо используйте SSE/streaming.

// ПЛОХО:
http.Get("https://api.example.com") // без ctx
// ХОРОШО:
req, _ := http.NewRequestWithContext(r.Context(), "GET", "https://api.example.com", nil)
http.DefaultClient.Do(req)

Передавайте r.Context() во все downstream-вызовы, иначе timeout middleware не работает.

c.Set("user", &User{})
v, _ := c.Get("user")
user := v.(*User) // panic, если что-то не так

gin использует map[string]any. Type-assertions опасны.

fiber на fasthttp не поддерживает HTTP/2. Это блокирует:

  • gRPC-Web (требует HTTP/2);
  • многие современные клиенты;
  • HTTP/2 server push (deprecated, но всё же).

Поэтому fiber не выбирают для production-API.


  1. Используйте net/http (1.22+) + chi для большинства проектов.
  2. Всегда ставьте таймауты на http.Server: ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout.
  3. Middleware: Logger, RequestID, Recoverer, Timeout, Metrics — стандартный набор.
  4. Передавайте r.Context() в downstream-вызовы (DB, HTTP-клиенты, Kafka).
  5. Используйте signal.NotifyContext для graceful shutdown.
  6. Не пишите в ResponseWriter после возврата из handler.
  7. Validator один глобальный, не создавайте на каждый запрос.
  8. Strict JSON parsing: DisallowUnknownFields() где можно.
  9. Метрики по route pattern, не по raw path.
  10. CORS — через готовую либу (rs/cors, chi/cors), не вручную.
  11. Auth — middleware, не в handler.
  12. Per-route middleware через r.Group() или r.With().
  13. Error responses — структурированные JSON, с кодом ошибки и сообщением.
  14. Не используйте fiber, если нужен HTTP/2 или совместимость с net/http экосистемой.
  15. OpenAPI spec — source of truth. spec-first для контракта.
  16. Health endpoints: /healthz (liveness) и /readyz (readiness) отдельно.
  17. Body size limit: http.MaxBytesReader(w, r.Body, 1<<20) против DDoS.
  18. TLS в production: используйте Let’s Encrypt (autocert), HTTP/2 включён по умолчанию.

  1. Что нового в http.ServeMux в Go 1.22? Method matching, path parameters (/users/{id}), wildcards ({path...}), host matching, precedence rules. Раньше нужен был chi/gorilla для этого.

  2. Чем net/http отличается от gin? net/http — stdlib, минимум, идиоматично. gin — фреймворк со своим Context, быстрее, но не совместим с net/http middleware.

  3. Что такое middleware в HTTP? Функция, оборачивающая http.Handler. Сигнатура func(next http.Handler) http.Handler. Применяется для logging, auth, recover, etc.

  4. Чем chi отличается от gin? chi совместим с net/http (Handler). gin использует свой gin.Context, не совместим. chi идиоматичнее для Go.

  5. Что такое http.HandlerFunc? Адаптер, превращающий функцию func(w, r) в http.Handler через метод ServeHTTP. Позволяет использовать обычные функции как handler.

  6. Зачем нужен ReadHeaderTimeout? Защита от Slowloris-атак: атакующий медленно шлёт заголовки, держа коннект. Без таймаута — DOS.

  7. Чем http.ErrServerClosed отличается от ошибки? Это «нормальное» возвращаемое значение ListenAndServe после Shutdown(). Игнорируйте через errors.Is.

  8. Как сделать graceful shutdown HTTP-сервера? srv.Shutdown(ctx) — даёт in-flight requests завершиться, потом закрывает listener. Запускайте по сигналу SIGTERM/SIGINT.

  9. Что делает middleware Timeout? Добавляет deadline в r.Context(). Чтобы handler реально прервался — он должен проверять ctx.Done() или передавать ctx в downstream.

  10. Как передать данные из middleware в handler? Через context.WithValue: создаёшь новый ctx, помещаешь значение, передаёшь r.WithContext(ctx) дальше. В handler — r.Context().Value(key).

  11. Чем отличается per-route middleware от global? Global — на всё (через Use). Per-route — внутри Group()/With()/Route(). В chi: r.With(mw).Get(...).

  12. Что такое http.Flusher? Интерфейс, позволяющий принудительно отправить буферизованные данные клиенту. Нужен для SSE и streaming.

  13. Когда выбрать fiber? Когда нужен экстремальный RPS, не нужен HTTP/2, нет переплетений с net/http экосистемой. Редкая ниша.

  14. Spec-first vs code-first для OpenAPI? Spec-first: spec — source of truth, генерируется сервер (oapi-codegen). Code-first: spec из комментариев (swag). Spec-first лучше для интеграции.

  15. Как сделать rate limiting? *golang.org/x/time/rate (token bucket). Per-IP — map[string]Limiter. Распределённый — Redis.

  16. Что такое CSRF и как защищаться? Cross-Site Request Forgery. Защита: SameSite cookies, CSRF-токены, Origin/Referer check. Не нужно для чистых JSON-API с Authorization header.

  17. Что такое CORS preflight? Браузер шлёт OPTIONS перед сложным запросом (не GET/POST с simple headers). Сервер отвечает Access-Control-Allow-*.

  18. Как вернуть streaming response? Установить Content-Type, писать в w и вызывать w.(http.Flusher).Flush() после каждого chunk.

  19. WebSocket: gorilla/websocket vs nhooyr.io? gorilla — старая популярная либа, недавно возобновлённая. nhooyr/coder — современная, без deps, проще API. Обе работают.

  20. Recover middleware — зачем? Чтобы panic в одном handler не убил весь сервер. Catch panic, log, вернуть 500.

  21. Как обработать http.ErrAbortHandler? Это специальный sentinel для прерывания обработки. В Recoverer middleware нужно re-panic, чтобы Go тёхнологически отменил response.

  22. Как лимитировать размер body? http.MaxBytesReader(w, r.Body, 1<<20) — обернуть body, чтобы Decode возвращал ошибку при превышении.

  23. Чем validate.Struct от validate.Var? Struct — валидирует struct по тегам. Var — одно значение с правилами строкой: validate.Var(email, "email").

  24. Что такое route pattern в chi? Шаблон роута без подстановки: /users/{id} (а не /users/123). Полезен для метрик, чтобы не плодить series.

  25. Можно ли использовать http.Server и grpc.Server на одном порту? Да, через cmux (soheilhy/cmux) или h2c upgrade. Но обычно разделяют порты (8080 HTTP, 9090 gRPC).

  26. Зачем DisallowUnknownFields()? Чтобы JSON с лишними полями возвращал ошибку. По умолчанию Go их игнорирует.

  27. Чем WriteTimeout отличается от IdleTimeout? Write — на одну response. Idle — между запросами в keep-alive connection.

  28. Что вернёт r.PathValue("missing") в Go 1.22? Пустую строку (""), не nil. PathValue для отсутствующего параметра не возвращает ошибку.

  29. Зачем нужен X-Request-ID header? Для distributed tracing — связать запрос на frontend с логами/трейсами на всех downstream-сервисах.

  30. Как мерить latency правильно? Histogram (не summary), с правильными buckets под SLO. Использовать route pattern, не raw path. Не считать времени до первой записи в response.


  1. Напишите HTTP-сервер на stdlib (1.22+) с эндпоинтами GET /users/{id}, POST /users, DELETE /users/{id}. Без фреймворка.

  2. Реализуйте middleware-chain:

    • RequestID;
    • Logger (метод, путь, статус, длительность);
    • Recoverer;
    • Timeout(5s);
    • Metrics (Prometheus). Покажите, что они применяются в правильном порядке.
  3. Переведите тот же сервер на chi. Добавьте /api/v1 с auth middleware, оставив /healthz public.

  4. Настройте go-playground/validator для CreateUserRequest. Возвращайте детали ошибок в JSON.

  5. Реализуйте SSE-эндпоинт /events, который пушит время раз в секунду. Проверьте, что r.Context().Done() отменяет отправку.

  6. Сравните три имплементации одного и того же API:

    • net/http + chi;
    • gin;
    • echo. На каком меньше всего кода?
  7. Напишите spec на 2-3 эндпоинта OpenAPI 3.0, сгенерируйте handlers через oapi-codegen, реализуйте логику.

  8. Настройте rate limiting на 100 req/s/IP. Покажите 429 ответ под нагрузкой (hey/vegeta).

  9. Протестируйте graceful shutdown: SIGTERM → новые connections отбиваются, in-flight завершаются за 30s.

  10. Реализуйте WebSocket-чат на gorilla/websocket: broadcast сообщений всем подключённым.