Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 9 из 25 ~16 мин +50 XP

Wildcards: маршруты со звёздочкой

Маршруты со звёздочкой в chi ловят хвост пути целиком: один шаблон /files/* закрывает и файлы любой вложенности, и SPA-приложение, чьи пути знает только браузерный роутер. Разберём chi wildcard на практике: что вернёт chi.URLParam(r, "*"), чем звёздочка отличается от {param}, что делать с /files/ без хвоста и почему /docs/*.pdf роняет сервер на старте.

Содержание урока · 10 разделов

В REST-каркасе курса chi каждый адрес был известен заранее: /notes, /notes/{id}, /notes?page=2 — шаблон перечисляет сегменты, и их число фиксировано. Для JSON API этого хватает, но добавьте к сервису файлы: отчёты лежат в files/reports/2024/q4.xls, картинки — в files/photos/trip/img.png, и глубина вложенности заранее неизвестна, потому что папки создают пользователи. Писать маршрут на каждый уровень — /files/{a}, /files/{a}/{b}, /files/{a}/{b}/{c} — значит проиграть бесконечную игру. Второй сюжет — SPA-приложение из урока про Mount и статику: браузерный роутер придумывает пути /dashboard/settings/team сам, и сервер обязан отдать index.html на любой из них. Обе задачи закрывает один инструмент — wildcard: шаблон со звёздочкой на конце, который ловит весь остаток пути, сколько бы сегментов в нём ни было. У инструмента три сюрприза: пустой хвост, жадность и запрет на фильтрацию по расширению — им и посвящён урок.

Маршруты со звёздочкой в chi: r.Get("/files/*", h)#

Звёздочка в шаблоне означает «весь остаток пути, включая слэши»: r.Get("/files/", listFiles) ловит и /files/a.txt, и /files/reports/2024/q4.xls — всё, что начинается с /files/. Хвост достаётся той же chi.URLParam, но под особым именем: chi.URLParam(r, "") вернёт reports/2024/q4.xls — без ведущего слэша, разделитель после /files роутер съедает при сопоставлении. Синтаксис документирован в README chi: рядом с named params вида /users/{userID} описан wildcard /admin/ и значение по ключу "". Два жёстких правила. Первое: звёздочка обязана быть последним символом шаблона — /files/.pdf не «маршрут с фильтром», а паника при регистрации, chi не даст даже запуститься; фильтрация переезжает в хендлер, разберём ниже. Второе: в шаблоне ровно одна звёздочка, и имени у неё нет — это фиксированный ключ "", двух разных wildcards в одном маршруте не бывает. В остальном это обычный маршрут: метод, хендлер, обычная регистрация.

Хвост пути целиком

GO
r.Get("/files/*", func(w http.ResponseWriter, r *http.Request) {
        tail := chi.URLParam(r, "*")

        if tail == "" {
                fmt.Fprint(w, "каталог /files/, хвост пуст")
                return
        }

        http.ServeFile(w, r, "./files/"+tail)
})

Хвост приходит одной строкой с внутренними слэшами: /files/reports/2024/q4.xls даёт reports/2024/q4.xls — разбирать по сегментам не нужно. http.ServeFile из net/http отдаёт файл с Content-Type и Last-Modified и отклоняет запросы, где в пути запроса есть «..», так что выход за корень ./files закрыт стандартной библиотекой. Ветка tail == "" срабатывает на адрес /files/ со слэшем и пустым хвостом — что с ней делать, отдельная тема ниже. Для запуска нужен локальный Go с каталогом ./files рядом с бинарником: песочница сайта файловую систему сервиса не видит.

Wildcard против {param}: один сегмент против хвоста#

На первый взгляд шаблоны /files/{name} и /files/ близнецы — оба «ловят что угодно после /files/». Разница в границах. {name} ловит ровно один сегмент: /files/q4.xls совпадёт, а /files/reports/q4.xls — нет, роутер ответит 404. Звёздочка ловит весь хвост с любым числом сегментов — слэши для неё не граница. Отсюда разное применение: {param} — для идентификаторов ресурса без слэшей, id заметки и slug статьи из [урока про URLParam](/courses/chi/lessons/04-urlparam/); wildcard — для файлов, SPA-путей и всего, что структурно «хвост», а не «сегмент». Третье различие — в именах: имя для {name} придумываете вы, у звёздочки оно всегда одно — "". Шаблоны можно смешивать: /hubs/{hubID}/view/ ловит параметр по имени и хвост под "" одновременно — chi так делает в собственных тестах, и значения приходят парой: hubID отдельно, хвост отдельно.

{param} и wildcard рядом#

Критерий{param}: /files/{name}Wildcard: /files/*
Что ловитодин сегмент до слэшавесь хвост, включая слэши
/files/reports/q4.xlsне совпадёт: два сегментасовпадёт: хвост целиком
Имя в URLParamпридумываете вы: "name"всегда "*"
Позиция в шаблонелюбая, можно несколькотолько в конце, ровно одна
Фильтр по расширениюнет — проверка в хендлеренет — и /files/*.pdf паникует
Типовое применениеid, slug, номерафайлы, SPA-фолбэк

Проверка: что вернёт звёздочка

Зарегистрирован r.Get("/files/*", h). Пришёл запрос GET /files/reports/2024/q4.xls. Что вернёт chi.URLParam(r, "*")?

SPA-фолбэк: index.html на любой незнакомый путь#

Одностраничное приложение живёт по правилу: сервер отдаёт index.html на любой путь, потому что /dashboard/settings/team существует только в JS-роутере браузера. Из урока про статику остался обещанный механизм: настоящие файлы монтируются на /assets, а wildcard-обёртка решает судьбу каждого хвоста на ветке приложения. Решение принимает простая проверка расширения: в хвосте есть точка — просят файл, его отдаёт ServeFile; точки нет — это путь роутера SPA, отдавайте index.html. Приём грубоватый и честно об этом говорит: стили и скрипты всегда с расширением, маршруты SPA — никогда; ломается он только на путях вида /download.tar.gz, которые тоже файлы, — тогда условие дополняется, а не выбрасывается. Для отдачи index.html годится тот же http.ServeFile с явным именем: заголовки и кэш поставит он же, отдельной логики не нужно.

Обёртка SPA с проверкой точки

GO
r.Handle("/app/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tail := chi.URLParam(r, "*")

        if strings.Contains(tail, ".") {
                http.ServeFile(w, r, "./static/"+tail)
                return
        }

        http.ServeFile(w, r, "./static/index.html")
}))

Handle без метода закрывает ветку на все HTTP-запросы — для SPA это то, что нужно: браузер стучится и GET-ом за разметкой, и OPTIONS-ом из CORS-логики. Хвост с точкой уходит в ServeFile с фактическим файлом, хвост без точки — в index.html: /app/settings вернёт разметку приложения со статусом 200. strings.Contains здесь сознательно примитивнее filepath.Ext — проверка спрашивает «похоже ли на файл?», а не «какое расширение?». Проверьте поведение локальным Go: curl /app/settings должен вернуть HTML, а /app/css/app.css — CSS.

Что получит клиент?

Прочитайте код и выберите, что он выведет:

r.Handle("/app/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tail := chi.URLParam(r, "*")
        if strings.Contains(tail, ".") {
                http.ServeFile(w, r, "./static/"+tail)
                return
        }
        w.Write([]byte("INDEX"))
}))

// в ./static лежат index.html и css/app.css
// запрос: GET /app/settings

Пустой хвост: /files/ и /files#

У звёздочки есть пограничный случай, о котором вспоминают по журналу ошибок. Адрес /files/ со слэшем попадает в маршрут /files/, и хвост оказывается пустой строкой: chi.URLParam(r, "") вернёт "". Это не сбой, а честный ответ роутера: хвоста нет. Что с ним делать — решение вашего API: ответить 404, отдать листинг каталога или приветствие, как в первом примере урока. А вот /files без слэша в этот маршрут не попадает вовсе: chi сопоставляет адрес посегментно, шаблон содержит слэш после /files, и без него совпадения нет — клиент получает 404. Лечится либо отдельной регистрацией r.Get("/files", ...), либо middleware.RedirectSlashes из пакета chi/middleware, который переадресует адреса к каноническому виду со слэшем. Отдельная регистрация честнее для API: статус переадресации и ручной маршрут не спорят между собой, а журнал остаётся чистым.

Проверка: звёздочка и расширение

Что произойдёт при регистрации r.Get("/docs/*.pdf", h)?

Конфликт маршрутов: что паникует, а что выигрывает#

Расхожее мнение: жадный wildcard глотает соседние маршруты. В chi карта приоритетов устроена иначе, и это хорошо видно по исходникам и тестам роутера: статичные ветки и ветка со звёздочкой — разные дети одного узла дерева, и при поиске статичные пробуются первыми. Специфичный маршрут /files/reports побеждает звёздочку /files/* при любом порядке регистрации — была ли звёздочка добавлена раньше или позже, значения не имеет: в тестах chi /admin/user уходит в статичный маршрут, /admin/hi — в catchall. Паника при конфликте паттернов в одной ветке — тоже не про chi: тот же шаблон, зарегистрированный дважды, молча перезаписывается, последний хендлер выигрывает, а вот повторный Mount на занятый путь роняет программу честно, с паникой на старте. Настоящая опасность — не регистрация, а хвосты глубже специфичного маршрута: /files/reports/q4.xls уйдёт в звёздочку, потому что у статичной ветки после reports продолжения нет.

Пять правил звёздочки#

  • Звёздочка — последняя в шаблоне и без текста после неё: /files/*.pdf роняет регистрацию с паникой.
  • Хвост достаётся chi.URLParam(r, "") одной строкой, без ведущего слэша; имя "" фиксировано.
  • {param} ловит один сегмент до слэша, wildcard — весь хвост: вложенные пути ловит только звёздочка.
  • /files/ даёт пустой хвост "", /files без слэша в маршрут /files/* не попадает вовсе.
  • Статичные маршруты сильнее звёздочки при любом порядке регистрации, но хвост глубже них уходит в wildcard.

Фильтр по расширению в хендлере

GO
r.Get("/docs/*", func(w http.ResponseWriter, r *http.Request) {
        tail := chi.URLParam(r, "*")

        if filepath.Ext(tail) != ".pdf" {
                http.Error(w, "только PDF", http.StatusNotFound)
                return
        }

        http.ServeFile(w, r, "./docs/"+tail)
})

Идея «/docs/*.pdf» в chi не существует — фильтр живёт здесь: filepath.Ext вырезает расширение из хвоста и сравнивает с ".pdf", чужой формат получает 404 ещё до обращения к диску. Ext вызывается у хвоста, а не у полного пути: для /docs/reports/q4.pdf он вернёт ".pdf". ServeFile после проверки отдаёт файл со стандартными заголовками; расширение — подсказка, а не гарантия: Content-Type клиент получит по фактическому содержимому файла.

Соберите SPA-обёртку

Восстановите регистрацию ветки, доставание хвоста, проверку расширения и отдачу фолбэка.

r.("/app/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tail := chi.(r, "*") if strings.(tail, ".") { http.ServeFile(w, r, "./static/"+tail) return } http.(w, r, "./static/index.html") }))

Соберите SPA-хендлер по строкам

Расставьте строки хендлера: от получения хвоста до отдачи файла или фолбэка.

Строки

Ваша программа

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

Карточки: wildcards

Термин

r.Get("/files/*", h)

нажмите, чтобы перевернуть
Определение

Маршрут со звёздочкой: ловит весь хвост пути после /files/ с любым числом сегментов. Звёздочка обязана быть последней в шаблоне, иначе chi паникует при регистрации.

1 / 6

Звёздочка закрыла последний пробел в адресах: фиксированные сегменты, {param} для одного, wildcard для хвоста любой длины — серверу не страшны ни файлы вглубь, ни браузерный роутер. Осталась задача из другого измерения: данные, которые не живут в адресе. Middleware проверил токен, сгенерировал id запроса — и должен донести это до хендлера, не заводя глобальных переменных. Инструмент — context.Context внутри каждого *http.Request, и следующий урок разбирает его по-взрослому: кастомный тип ключа, helper-функции и цена строкового "user" из туториалов. Домашнее задание ниже собирает обе ветки этого урока — файлы и SPA — в один роутер: curl по трём адресам покажет разницу между файлом, фолбэком и пустым хвостом.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Шаблон /files/* ловит весь хвост пути с любым числом сегментов; хвост достаётся chi.URLParam(r, "*") одной строкой без ведущего слэша.
  • Звёздочка обязана быть последней в шаблоне: /docs/*.pdf роняет регистрацию паникой, фильтр по расширению пишется в хендлере через filepath.Ext.
  • {param} ловит один сегмент до слэша, wildcard — весь хвост: вложенные пути и SPA-адреса ловит только звёздочка.
  • /files/ даёт пустой хвост "" и решение принимает хендлер; /files без слэша в маршрут /files/* не попадает вовсе.
  • Статичные маршруты chi пробует раньше звёздочки при любом порядке регистрации; повторный Mount на занятый путь паникует.
  • Хвост глубже специфичного маршрута уходит в wildcard: вместо отчёта может прийти index.html со статусом 200 — тихий способ сломать API-клиента.
  • SPA-фолбэк — wildcard-обёртка: точка в хвосте — файл через http.ServeFile, без точки — index.html.

Домашнее задание

Соберите в main.go две ветки: /files/* с отдачей файлов через http.ServeFile и 404 на пустой хвост, и /app/* как SPA-фолбэк с проверкой точки. Добавьте r.Get("/files/reports") и проверьте curl-ом три адреса: конкретный файл, вложенный хвост и /app/settings — третий должен вернуть HTML со статусом 200.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Wildcards: маршруты со звёздочкой)

Вопросы и ответы по уроку

Чем wildcard в chi отличается от звёздочки в Gin?

Синтаксисом и способом достать значение: chi ловит хвост шаблоном /files/* и отдаёт его через chi.URLParam(r, "*"), Gin пишет /files/*filepath и достаёт c.Param("filepath"). Механика та же — хвост одной строкой; а вот правила приоритета соседних маршрутов в роутерах различаются, chi старается специфичное.

Как отдать только PDF через wildcard?

Никак на уровне шаблона: /docs/*.pdf паникует, звёздочка не умеет фильтровать. Фильтр пишется в хендлере: filepath.Ext(chi.URLParam(r, "*")) сравнивается с ".pdf", и на несовпадение хендлер отвечает 404 ещё до обращения к диску.

Почему /files отвечает 404, а /files/ работает?

Шаблон /files/* содержит слэш после /files, и chi сопоставляет адрес посегментно: без хвостового слэша совпадения нет. Либо регистрируйте отдельный r.Get("/files", ...), либо включите middleware.RedirectSlashes — он переадресует адрес к каноническому виду со слэшем.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter