Shanraq.org Shanraq.org
HTTP по-настоящему: метод, путь, заголовки и код ответа
IT

Go: с нуля до своего блога Урок 19 из 50

HTTP по-настоящему: метод, путь, заголовки и код ответа

Девятнадцатый урок курса по Go и начало модуля о вебе. Запрос — это структура, которую можно прочитать: метод, путь, заголовки, параметры. Ответ уходит в одну сторону: заголовки, код, тело — и в этом порядке. Почему 404 и 405 разные и что значит superfluous WriteHeader.

Зачем это нужно

В первом уроке вы посмотрели на HTTP снаружи, командой curl. В третьем написали сервер, который отвечает одной строкой. Между этими двумя точками осталось всё остальное: как прочитать запрос и как собрать ответ осознанно, а не одной строкой на все случаи.

Отсюда и начинается настоящий блог. Пока сервер не различает, что у него спросили, у него не может быть ни страниц, ни языков, ни ошибок.

Сразу целиком

Новая папка, go mod init sabaq17, main.go:

package main

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

func handle(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		w.Header().Set("Allow", "GET")
		w.WriteHeader(http.StatusMethodNotAllowed)
		fmt.Fprintln(w, "только GET")
		return
	}

	if r.URL.Path != "/" {
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintln(w, "такой страницы нет:", r.URL.Path)
		return
	}

	lang := r.URL.Query().Get("lang")
	if lang == "" {
		lang = "kz"
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprintln(w, "метод:", r.Method)
	fmt.Fprintln(w, "путь:", r.URL.Path)
	fmt.Fprintln(w, "язык:", lang)
	fmt.Fprintln(w, "браузер:", r.Header.Get("User-Agent"))
}

func main() {
	http.HandleFunc("/", handle)
	log.Println("http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Запустите go run . и откройте localhost:8080 в браузере. А потом посмотрите на ответ целиком — флаг -i показывает заголовки:

curl -i "http://localhost:8080/?lang=ru"

Прежде чем смотреть ниже — скажите вслух, какой код ответа придёт. Потом сверьте:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sat, 05 Sep 2026 04:38:30 GMT
Content-Length: 66

метод: GET
путь: /
язык: ru
браузер: curl/8.4

Теперь спросите несуществующую страницу и чужим методом:

curl -i http://localhost:8080/kokek
curl -i -X POST http://localhost:8080/
HTTP/1.1 404 Not Found
...
такой страницы нет: /kokek
HTTP/1.1 405 Method Not Allowed
Allow: GET

Разбор

Запрос — структура, и вы её уже видели

r *http.Request — та самая звёздочка из урока про указатели. Вам дают адрес запроса, а не его копию: запрос большой, и копировать его на каждый вызов было бы расточительно.

Внутри — поля, к которым вы обращаетесь через точку, как к любой структуре:

r.Method        // "GET"
r.URL.Path      // "/kokek"
r.Header        // все заголовки

Ничего нового в языке здесь нет. Новое — только имена полей.

Образ. Заполненный бланк. Наверху написано, что просят делать (метод) и с чем (путь), ниже графы с подробностями (заголовки), в конце — приложение, если есть (тело). Вы читаете бланк, а не догадываетесь.

Метод и путь

Метод говорит, что сделать: GET — покажи, POST — прими. Путь говорит, с чем.

Проверять их надо самому. Обработчик, зарегистрированный на /, получает все пути, которые никто другой не забрал, — включая /kokek. Поэтому в программе стоит явная проверка r.URL.Path != "/", а иначе несуществующая страница ответила бы 200 и главной страницей.

Имена методов лучше брать из констант — http.MethodGet вместо "GET": опечатку в строке компилятор не поймает, а в имени константы поймает.

Заголовки: прочитать и поставить

r.Header.Get("User-Agent")                        // прочитать
w.Header().Set("Content-Type", "text/plain")      // поставить

Чтение — из запроса, запись — в ответ, и путать их некуда: у запроса Header поле, у ответа Header() метод.

Если Content-Type не поставить, Go угадает его по первым байтам тела. В нашем 404 мы ничего не ставили — и в ответе всё равно появился Content-Type: text/plain; charset=utf-8. Удобно, но полагаться на угадывание не стоит: HTML он опознает, а вот ваш собственный формат — нет.

Код состояния и порядок

Вот главное правило урока, и оно стоит того, чтобы проверить его руками.

w.Header().Set("Content-Type", "application/json")   // 1. заголовки
w.WriteHeader(http.StatusNotFound)                   // 2. код
fmt.Fprintln(w, "{}")                                // 3. тело

Только в этом порядке. Ответ уходит в сеть по мере того, как вы его пишете, и вернуть уже отправленное нельзя.

Поменяйте местами первые две строки — заголовок не уедет: в ответе окажется тип, угаданный Go, а не ваш. Напишите тело до WriteHeader — код навсегда останется 200, а в журнал сервера упадёт:

http: superfluous response.WriteHeader call from main.main.func3 (main.go:22)

Образ. Конверт в почтовом ящике. Пока конверт у вас — пишите на нём что угодно. Опустили в ящик — надписывать поздно, и достать обратно нельзя.

Отдельно предупрежу: если вы решите проверить это тестом через httptest.NewRecorder, поздний заголовок в нём будет виден. Записыватель хранит заголовки в обычной карте и отдаёт её в конце, а настоящее соединение — нет. Проверять такое надо на живом сервере.

Тело: w — это интерфейс

fmt.Fprintln(w, …) работает потому, что w умеет принимать байты. Это io.Writer — интерфейс из урока про договоры, и та же функция пишет в файл, в буфер и в сеть, не зная разницы.

Отсюда и приём, которым мы пользовались весь модуль о языке: логику пишут так, чтобы она писала в io.Writer, — и её можно проверить тестом, ничего не запуская.

Параметры запроса

lang := r.URL.Query().Get("lang")

Всё, что после ?, — параметры. Query() разбирает их в карту, Get достаёт по имени и возвращает пустую строку, если параметра нет. Никаких ошибок, никаких паник — поэтому значение по умолчанию ставите вы сами.

Именно так на этом сайте выбирается язык статьи: ?lang=kz.

404 и 405 — разные ответы

Их путают, а разница простая. 404 — такой страницы нет вообще. 405 — страница есть, но так с ней нельзя: вы прислали POST туда, где принимают только GET.

И у 405 есть обязанность: заголовок Allow со списком того, что можно. Вы видели его в ответе выше — это не украшение, а часть протокола.

Образ. Закрытая дверь и не та дверь. «Такого кабинета нет» и «кабинет есть, но приём по средам» — разные ответы, и человек по ним делает разное.

Карта урока

Карта урока: ответ уходит в одну сторону — заголовки, код, тело

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему обработчик, зарегистрированный на /, обязан сам проверять путь?
  2. Что произойдёт, если поставить заголовок после w.WriteHeader?
  3. Когда отвечают 404, а когда 405?

Задание

Обязательное. Допишите сервер так, чтобы он отвечал на три пути: / — приветствие, /about — строчка о себе, всё остальное — 404 с внятным текстом. На /about разрешите только GET, на всё остальное отвечайте 405 с заголовком Allow. Проверьте все случаи через curl -i и убедитесь, что коды именно те, что задуманы.

По желанию.

  • Добавьте ?lang=: на kz, ru и en отвечайте на своём языке, на неизвестное значение — 400 с объяснением.
  • Поставьте свой заголовок X-Powered-By и найдите его в ответе curl -i.
  • Напишите тело до WriteHeader и посмотрите на строку в журнале сервера своими глазами.

Куда это встанет в блоге

Этот урок — фундамент модуля. В следующем пути разъедутся по обработчикам и проверять r.URL.Path руками больше не придётся. Дальше появятся шаблоны, и в тело ответа поедет HTML, а не строки. Но порядок — заголовки, код, тело — останется тем же до конца курса.

Ответы

Показать ответы
  1. Потому что / в стандартном маршрутизаторе означает «всё, что не забрал никто другой». Без проверки любая несуществующая страница получила бы код 200 и содержимое главной, а поисковик решил бы, что у сайта тысячи одинаковых страниц.
  2. Ничего не произойдёт — и в этом беда. Заголовки уже ушли вместе с кодом, поздний Set меняет только карту в памяти. В ответе окажется то, что Go угадал сам.
  3. 404 — когда такого адреса нет вовсе. 405 — когда адрес есть, но метод для него не разрешён; вместе с 405 обязательно отдают заголовок Allow со списком допустимых методов.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.