Многие 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.