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

HTTP-сервер на net/http: регистрация обработчиков и формирование ответа

Содержание курса

Почему неизвестный путь внезапно отвечает как главная страница

В предыдущем примере зарегистрированы /hello и /. Второй шаблон в ServeMux охватывает не только главную страницу, но и другие пути, если для них нет более точного обработчика. Поэтому запрос на /about может получить текст root со статусом 200 вместо ожидаемой ошибки 404. Это легко пропустить, проверив только рабочий маршрут.

Если обработчик главной страницы должен принимать ровно /, проверьте путь внутри него. Ниже полный маленький пример: httptest создаёт тестовые запросы и записывает ответы без открытия сетевого порта.

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        fmt.Fprint(w, "root")
    })
    for _, path := range []string{"/", "/about"} {
        w := httptest.NewRecorder()
        mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
        fmt.Println(path, w.Code)
    }
}

Вывод: / 200 и /about 404. Уберите проверку r.URL.Path и повторите: второй статус станет 200. Так вы проверяете не только запуск сервера, но и смысл маршрутов. В Go 1.22+ существует также шаблон /{$} только для корня, однако проверка пути внутри обработчика понятна и работает в более старом коде. Для остальных адресов регистрируйте отдельные обработчики по мере появления настоящих страниц. Следующий шаг после корректных статусов — ответы API в JSON.

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

Начать заново?

Помощник начнёт новый диалог по этой задаче. Текущая переписка останется в истории до конца срока хранения.

О переписке

Можно приложить до трёх фото PNG, JPEG или WebP по 5 МБ.

Переписка и фото хранятся 90 дней после последнего принятого сообщения. Преподаватель получит доступ к диалогу, когда вы попросите его помочь.

Проверка решения остаётся отдельным действием.