Урок по треку Go 7 мин

net/http и middleware

Handler и ServeMux с шаблонами Go 1.22, middleware, таймауты сервера, клиент и закрытие Body, graceful shutdown.

Многие Go-сервисы обходятся стандартной net/http, особенно после Go 1.22, где роутер научился методам и параметрам пути. Фреймворки (chi, gin, echo, fiber) строятся на тех же идеях.

Handler

type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

http.HandlerFunc превращает обычную функцию в Handler. Каждый запрос обрабатывается в своей горутине, поэтому общее состояние обработчиков нужно защищать.

ServeMux Go 1.22+

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /static/", serveStatic)      // префикс
mux.HandleFunc("GET /{$}", home)                 // только корень

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
}
  • Метод в шаблоне; GET заодно обслуживает HEAD. На неверный метод ответ 405 с заголовком Allow.
  • Из пересекающихся шаблонов выигрывает более конкретный. Если ни один не конкретнее, регистрация паникует.
  • Шаблон с / на конце совпадает с префиксом, {$} отключает это.

Ответ

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)

Порядок важен: заголовки до WriteHeader, WriteHeader до тела. Первый Write без WriteHeader отправит 200. Изменения заголовков после этого уже не уйдут.

Middleware

Middleware это функция func(http.Handler) http.Handler:

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("req", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}

handler := recoverer(logging(auth(mux)))

Внешний оборачивает внутренний: запрос проходит recoverer, logging, auth, mux. Чтобы узнать статус ответа, middleware оборачивает ResponseWriter своим типом с перехватом WriteHeader. Данные в обработчик передают через r.WithContext(context.WithValue(...)).

Таймауты сервера

http.ListenAndServe без настроек не имеет таймаутов: медленный клиент может держать соединения вечно (атака slowloris).

srv := &http.Server{
    Addr:              ":8080",
    Handler:           handler,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       10 * time.Second,
    WriteTimeout:      15 * time.Second,
    IdleTimeout:       60 * time.Second,
}

Ограничение тела: http.MaxBytesReader(w, r.Body, 1<<20). Тайм-аут обработки: http.TimeoutHandler или контекст.

Graceful shutdown

srv.Shutdown(ctx) закрывает слушающие сокеты, закрывает простаивающие соединения и ждёт завершения активных запросов до дедлайна контекста. ListenAndServe после этого сразу возвращает http.ErrServerClosed, это не ошибка.

Клиент

client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil { return err }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
  • http.DefaultClient без таймаута: зависший сервер подвесит горутину навсегда.
  • Тело ответа нужно закрыть всегда, иначе соединение не вернётся в пул и потекут сокеты и горутины. Для переиспользования соединения тело ещё и дочитывают.
  • Ошибка сети приходит в err, а статус 500 нет: resp.StatusCode проверяйте сами.
  • Transport держит пул keep-alive соединений; MaxIdleConnsPerHost по умолчанию 2, при высоком RPS к одному хосту его увеличивают.
  • Создавайте клиент один раз и переиспользуйте.

Что спросят на собесе

  • Что такое Handler и HandlerFunc.
  • Как написать middleware и в каком порядке они выполняются.
  • Что нового в ServeMux Go 1.22.
  • Какие таймауты нужны серверу и клиенту.
  • Почему важно закрывать resp.Body и как сделать graceful shutdown.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.