Shanraq.org Shanraq.org
Маршруты в Go: кто обрабатывает какой адрес
IT

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 лучше по трём причинам: он виден в коде, его можно передать в тест, и два разных сервера в одной программе не мешают друг другу.

Карта урока

Карта урока: один вход, несколько подписанных дверей, выбирает маршрутизатор

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

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

  1. Чем GET / отличается от GET /{$}?
  2. Вы зарегистрировали GET /read/{slug} и GET /read/dala. Куда попадёт /read/dala и зависит ли это от порядка строк?
  3. Откуда берётся 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.

Ответы

Показать ответы
  1. GET / ловит все пути, которые не подошли другим образцам, включая несуществующие. GET /{$} ловит только сам корень: {$} означает, что путь на этом и заканчивается. Без него любая опечатка в адресе вернула бы главную страницу с кодом 200.
  2. В GET /read/dala, потому что он конкретнее. Порядок строк не имеет значения: маршрутизатор выбирает не первый подошедший образец, а самый точный.
  3. Его добавляет ServeMux. Он знает все образцы для этого пути и отвечает 405 со списком разрешённых методов. HEAD в списке появляется потому, что это тот же GET без тела, и Go обрабатывает его сам.

Источники

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

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

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

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

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

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