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 со списком того, что можно. Вы видели его в ответе выше — это не украшение, а часть протокола.
Образ. Закрытая дверь и не та дверь. «Такого кабинета нет» и «кабинет есть, но приём по средам» — разные ответы, и человек по ним делает разное.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему обработчик, зарегистрированный на
/, обязан сам проверять путь? - Что произойдёт, если поставить заголовок после
w.WriteHeader? - Когда отвечают 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, а не строки. Но порядок — заголовки, код, тело — останется тем же до конца курса.
Ответы
Показать ответы
- Потому что
/в стандартном маршрутизаторе означает «всё, что не забрал никто другой». Без проверки любая несуществующая страница получила бы код 200 и содержимое главной, а поисковик решил бы, что у сайта тысячи одинаковых страниц. - Ничего не произойдёт — и в этом беда. Заголовки уже ушли вместе с кодом, поздний
Setменяет только карту в памяти. В ответе окажется то, что Go угадал сам. - 404 — когда такого адреса нет вовсе. 405 — когда адрес есть, но метод для него не разрешён; вместе с 405 обязательно отдают заголовок
Allowсо списком допустимых методов.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.