Go: с нуля до своего блога Урок 20 из 50
Маршруты в Go: кто обрабатывает какой адрес
Двадцатый урок курса по Go. ServeMux разводит запросы по обработчикам: метод и путь пишутся одной строкой, {slug} вынимает кусок адреса, {$} не даёт главной странице ловить всё подряд. Ошибки 404 и 405 маршрутизатор берёт на себя, а конфликт двух образцов ловит при запуске.
Зачем это нужно
В прошлом уроке обработчик проверял путь и метод сам:
if r.Method != http.MethodGet { ... }
if r.URL.Path != "/" { ... }
Это работает ровно до второй страницы. На пятой такой обработчик превращается в лестницу из if, где легко забыть ветку и отдать главную страницу вместо 404.
Пусть адреса разводит тот, чья это работа, — маршрутизатор.
Сразу целиком
Новая папка, go mod init sabaq18, main.go:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "главная")
})
mux.HandleFunc("GET /about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "об авторе")
})
mux.HandleFunc("GET /read/{slug}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "статья:", r.PathValue("slug"))
})
mux.HandleFunc("POST /read/{slug}/like", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "понравилось:", r.PathValue("slug"))
})
log.Println("http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Запустите и проверьте все случаи. Прежде чем смотреть ниже — скажите вслух, каким будет ответ на GET /read/dala/like:
GET / → 200 главная
GET /about → 200 об авторе
GET /read/dala → 200 статья: dala
POST /read/dala/like → 200 понравилось: dala
GET /read/dala/like → 405 Allow: POST
GET /kokek → 404
POST /about → 405 Allow: GET, HEAD
Ни одной проверки пути и метода в коде больше нет — а ответы стали точнее, чем были.
Разбор
Метод и путь — одной строкой
mux.HandleFunc("GET /about", ...)
До Go 1.22 в образец писали только путь, а метод проверяли внутри обработчика. Теперь метод — часть образца, и лишний if исчез.
Отсюда правило: один образец — одно действие. GET /read/{slug} показывает статью, POST /read/{slug}/like ставит отметку. Это разные строки и разные функции, а не одна с развилкой внутри.
Образ. Подписанные двери. В учреждении не спрашивают у одного окошка всё подряд: над каждым написано, что здесь делают. Вы читаете надпись и идёте к нужному.
{slug} — кусок адреса как значение
mux.HandleFunc("GET /read/{slug}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "статья:", r.PathValue("slug"))
})
Фигурные скобки в образце означают: здесь стоит любое значение, назови его slug. Достают его методом r.PathValue("slug").
Имя внутри скобок ваше — назовите {id} или {name}, лишь бы PathValue спрашивал то же самое. И на этом сайте адрес статьи устроен ровно так: /read/go-marshruttar.
{$} — иначе главная поймает всё
Образец GET / в Go означает не «главная страница», а «всё, что не забрал никто другой». Тот самый случай из прошлого урока.
Проверьте сами: замените GET /{$} на GET / — и /kokek вместо 404 получит главную страницу с кодом 200.
{$} в конце образца значит «путь заканчивается здесь». GET /{$} — только сама главная, и ничего больше.
Образ. Ящик «всё остальное». Такой ящик полезен, пока в него кладут действительно всё остальное. Но если написать на нём «входящие», туда посыплется и почта, и мусор, и вы это заметите не сразу.
Кто побеждает, если подходят двое
Порядок регистрации не имеет значения. Побеждает более конкретный образец:
mux.HandleFunc("GET /read/{slug}", ...) // любая статья
mux.HandleFunc("GET /read/dala", ...) // именно эта
/read/dala попадёт во второй, /read/kokek — в первый, и неважно, в каком порядке вы их написали.
А если два образца ловят одно и то же — программа не запустится:
panic: pattern "GET /read/{name}" conflicts with pattern "GET /read/{slug}":
GET /read/{name} matches the same requests as GET /read/{slug}
Паника при старте выглядит грубо, но это подарок: конфликт маршрутов нашли вы на своей машине, а не читатель в проде.
Образ. Два одинаковых указателя на развилке. Пока их не сняли, ехать нельзя — и правильно, что машина останавливается сразу, а не заезжает наугад.
404 и 405 маршрутизатор берёт на себя
Ни в одном обработчике нет ни WriteHeader(404), ни Allow. Но ответы правильные, потому что ServeMux знает все образцы: путь не подошёл ни одному — 404; путь подошёл, а метод нет — 405 со списком разрешённых.
Обратите внимание на список: Allow: GET, HEAD. HEAD появился сам — это тот же GET, но без тела, и Go отвечает на него за вас.
Когда маршрутов станет много
Через десяток строк регистрации main перестанет читаться. Обычный приём — вынести маршруты в отдельную функцию, которая возвращает готовый маршрутизатор:
func routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
mux.HandleFunc("GET /about", about)
mux.HandleFunc("GET /read/{slug}", article)
return mux
}
Тогда в main остаётся одна строка, а все адреса сайта видны в одном месте, списком. И этот же список можно отдать в тест, не поднимая настоящий сервер.
nil, который был во втором аргументе
http.ListenAndServe(":8080", mux)
С третьего урока там стоял nil. Теперь можно сказать, что это значило: «возьми маршрутизатор по умолчанию» — тот самый, в который писали http.HandleFunc без mux.
Свой mux лучше по трём причинам: он виден в коде, его можно передать в тест, и два разных сервера в одной программе не мешают друг другу.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Чем
GET /отличается отGET /{$}? - Вы зарегистрировали
GET /read/{slug}иGET /read/dala. Куда попадёт/read/dalaи зависит ли это от порядка строк? - Откуда берётся
Allow: GET, HEAD, если вы его нигде не писали?
Задание
Обязательное. Разведите блог по маршрутам: GET /{$} — список статей, GET /read/{slug} — одна статья по адресу, GET /about — о себе. Статьи возьмите из среза структур, а если такого адреса нет — отвечайте 404 своим текстом. Проверьте все случаи через curl -i, включая POST /about.
По желанию.
- Замените
GET /{$}наGET /и посмотрите, что станет с/kokek. Верните обратно. - Зарегистрируйте два образца, ловящих одно и то же, и прочитайте панику целиком.
- Добавьте
GET /read/{slug}/{part}и посмотрите, что придёт вPathValueдля обоих имён.
Куда это встанет в блоге
Адреса блога уже почти настоящие: /, /about, /read/{slug}. Не хватает только содержимого — пока в ответ уходят строки, а не страницы.
В следующем уроке появится обёртка, которая делает одно и то же для всех маршрутов сразу: журнал, время ответа, проверка. А затем — шаблоны, и вместо fmt.Fprintln в тело поедет HTML.
Ответы
Показать ответы
GET /ловит все пути, которые не подошли другим образцам, включая несуществующие.GET /{$}ловит только сам корень:{$}означает, что путь на этом и заканчивается. Без него любая опечатка в адресе вернула бы главную страницу с кодом 200.- В
GET /read/dala, потому что он конкретнее. Порядок строк не имеет значения: маршрутизатор выбирает не первый подошедший образец, а самый точный. - Его добавляет
ServeMux. Он знает все образцы для этого пути и отвечает 405 со списком разрешённых методов.HEADв списке появляется потому, что это тот жеGETбез тела, и Go обрабатывает его сам.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.