HTTP-сервер на net/http: регистрация обработчиков и формирование ответа
Содержание курса
В прошлом уроке мы передавали ctx первым аргументом и следили за его отменой через select. Здесь та же дисциплина появится в другом месте: каждый HTTP-обработчик получает запрос r, у которого есть r.Context() — и паттерн, уже знакомый по WithCancel/WithTimeout, работает там без изменений. Но сначала нужно разобраться, как вообще устроен сервер: как зарегистрировать обработчики, запустить сервер и правильно сформировать ответ.
http.NewServeMux() и mux.HandleFunc: изолированный маршрутизатор с зарегистрированными путями
Пакет net/http содержит глобальный маршрутизатор по умолчанию — http.DefaultServeMux. Если вызывать http.HandleFunc("/path", ...) без явного mux, обработчик попадает именно туда. Проблема в том, что DefaultServeMux — глобальная переменная пакета: любой импортированный пакет может зарегистрировать в ней свои пути, и это может привести к неожиданным конфликтам. Поэтому практически всегда лучше создавать собственный маршрутизатор:
mux := http.NewServeMux()
Эта строка возвращает новый, пустой *http.ServeMux, никак не связанный с глобальным. Всё, что регистрируется на mux, существует только в нём.
Регистрация обработчика выглядит так:
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello!")
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "root")
})
Сигнатура функции-обработчика фиксирована: func(w http.ResponseWriter, r *http.Request). http.ResponseWriter — это интерфейс, через который мы пишем ответ; *http.Request — структура с данными входящего запроса.
Теперь про разницу между "/hello" и "/". Путь без завершающего слеша — точное совпадение: "/hello" сработает только для запроса ровно на /hello. Путь с завершающим слешем — префиксное совпадение: "/" поймает всё, что не совпало с более конкретным паттерном. Если зарегистрировать только "/hello", то запрос на /about вернёт 404. Если добавить "/", то /about попадёт туда.
Один mux может содержать сколько угодно маршрутов — просто вызываете HandleFunc несколько раз.
Ещё один момент: если позже понадобится тестировать обработчики изолированно (без реального порта), удобно оформить создание маршрутизатора в виде функции-конструктора:
func newRouter() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/", rootHandler)
return mux
}
Каждый вызов newRouter() возвращает свежий независимый *http.ServeMux. Это позволяет в тестах создавать отдельный экземпляр маршрутизатора для каждого теста, не мешая другим.
