HTTP-сервер, JSON API и итоговый проект

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. Это позволяет в тестах создавать отдельный экземпляр маршрутизатора для каждого теста, не мешая другим.