Урок курса
HTTP-сервер на net/http: регистрация обработчиков и формирование ответа
Golang с нуляВ прошлом уроке мы передавали 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. Это позволяет в тестах создавать отдельный экземпляр маршрутизатора для каждого теста, не мешая другим.
http.ListenAndServe: блокирующий запуск, обработка ошибки и рабочий цикл с двумя терминалами
Когда маршрутизатор готов, его нужно передать серверу:
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
http.ListenAndServe принимает адрес для прослушивания и http.Handler — интерфейс, который реализует *http.ServeMux. Функция открывает TCP-сокет на порту 8080 и входит в бесконечный цикл приёма соединений. Она блокирует текущую горутину — то есть строки после неё в нормальном режиме работы никогда не выполнятся.
Именно поэтому нельзя просто написать http.ListenAndServe(...) и продолжать код ниже: сервер занял поток и ждёт запросов.
Что происходит на практике:
- Запускаете программу в первом терминале — он «зависает» (это нормально, сервер работает).
- Открываете второй терминал и отправляете запрос:
# Linux / macOS
curl http://localhost:8080/hello
# Windows PowerShell
Invoke-WebRequest http://localhost:8080/hello
- Сервер обрабатывает запрос, первый терминал может показать логи, второй — ответ.
- Чтобы остановить сервер, нажимаете
Ctrl+Cв первом терминале.
Почему log.Fatal? ListenAndServe возвращает ошибку только в одном случае: что-то пошло не так при запуске или во время работы — например, порт уже занят другим процессом. Игнорировать эту ошибку нельзя: без неё вы не узнаете, что сервер вообще не стартовал. log.Fatal выводит сообщение и завершает программу с ненулевым кодом.
Типичная ошибка при занятом порте выглядит так:
2024/01/15 10:23:45 listen tcp :8080: bind: address already in use
Если написать http.ListenAndServe(":8080", mux) без проверки — программа завершится молча или продолжит выполнение с незапущенным сервером, и отлаживать это будет неудобно.
r.Method, r.URL.Path и порядок Header → WriteHeader → Write при формировании ответа
Внутри обработчика r *http.Request содержит всё о входящем запросе. Два самых частых поля:
r.Method— строка с HTTP-методом:"GET","POST","DELETE"и т.д.r.URL.Path— строка с путём запроса, например"/hello".
Пример использования:
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, "only GET allowed")
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Hello from %s\n", r.URL.Path)
}
Порядок вызовов строго обязателен: сначала w.Header().Set(...), затем w.WriteHeader(code), затем запись тела через w.Write(...) или fmt.Fprintf(w, ...).
Почему порядок нельзя нарушать? HTTP-ответ устроен так: сначала идёт статусная строка и заголовки, потом тело. Как только вы начинаете писать тело, заголовки уже уходят клиенту — изменить их невозможно. net/http реализует это жёстко: первый вызов Write или Fprintf автоматически отправляет статус 200 и все текущие заголовки, если WriteHeader ещё не был вызван.
Что будет, если нарушить порядок:
// Неправильно!
func badHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "oops") // ← здесь неявно уходит статус 200
w.WriteHeader(http.StatusTeapot) // ← этот вызов игнорируется, уже поздно
}
Клиент получит статус 200, а в логах Go напишет что-то вроде:
http: superfluous response.WriteHeader call from ...
Это предупреждение в stderr, но ответ уже отправлен с неверным статусом.
Одно исключение: если вы отвечаете с кодом 200 и не устанавливаете специальных заголовков, WriteHeader(200) можно не вызывать — первый Write сделает это сам. Но как только нужен другой статус или любой заголовок — следуйте порядку явно.
Попробуйте решить
Обработчик пишет код:
fmt.Fprintf(w, "hello")
w.WriteHeader(201)
Какой HTTP-статус получит клиент и почему?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
