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

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

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

Урок 3 из 25 ~20 мин +50 XP

Маршрутизация в Echo: параметры пути, query и приоритеты

Маршрутизация в Echo — это умение описать /users/42, /users/42/posts/7 и /search?q=echo&page=2, достать значения через c.Param и c.QueryParam и предсказать, какой маршрут победит, если под запрос подходят два. Разбираем path-параметры, wildcard и query на коде, который можно скопировать в проект из прошлого урока.

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

Любой API живёт на двух видах данных в URL. Первое — путь: /users/42 означает «пользователь номер 42», и 42 здесь — часть маршрута. Второе — строка запроса после знака вопроса: /search?q=echo&page=2 — уточнения к тому же пути, например номер страницы. В этом уроке научимся регистрировать такие маршруты в Echo и доставать из них значения, а заодно разберём правило, которое в первый день обязательно путает новичков: что вернуть, когда один запрос подходит под два маршрута сразу. Всё проверяемое здесь выполняется локально: дописывайте маршруты в main.go из прошлого урока и смотрите ответы через curl.

Как работает роутер Echo#

Маршруты Echo живут в radix-дереве — дереве из префиксов путей, где поиск совпадения выглядит как спуск от корня по общим частям строк. Заявление из readme — поиск маршрута без динамических аллокаций памяти — на практике означает, что сопоставление запроса с маршрутом не создаёт мусора для сборщика мусора: на больших нагрузках это часть экономии. Важнее для повседневной работы другое — правила приоритета. Если под запрос подходят несколько маршрутов, Echo выбирает самый конкретный: статический сегмент (/users/new) важнее параметра (:id), параметр важнее wildcard (*). Дубликатов в дереве не бывает: повторная регистрация одного и того же метода и пути вызывает панику при старте — сразу видно, что маршруты в проекте конфликтуют. И обратите внимание: порядок объявления маршрутов, в отличие от многих фреймворков, на результат не влияет — важна только специфичность правила.

Path-параметры: :id и c.Param

GO
e.GET("/users/:id", func(c echo.Context) error {
        id := c.Param("id")
        return c.JSON(http.StatusOK, map[string]string{"id": id})
})

Двоеточие в пути объявляет параметр: :id означает «в этом месте один сегмент, любое значение». Значение достаётся c.Param("id") по имени — всегда строка. Запросы /users/42 и /users/anna попадут в один обработчик с id = "42" и id = "anna". А вот /users/42/posts/7 сюда уже не попадёт: :id матчит ровно один сегмент.

Числа из параметров и ошибка 400#

Параметр — всегда строка, и «42» из URL — это не число. Если id по смыслу числовой, конвертируйте вручную через strconv.Atoi и честно обработайте неудачу: /users/abc — это не ошибка сервера, а запрос клиента с мусором, и отвечать надо 400, а не 500. Возвращать ошибку будем через echo.NewHTTPError, знакомую с прошлого урока: центральный обработчик превратит её в ответ со статусом и сообщением. Шаблон ниже — типовая обёртка, которая потом разрастётся в настоящий обработчик с базой данных, — сохраните его в проект: он переезжает из сервиса в сервис почти без изменений.

getUser: параметр, конвертация, ответ

GO
func getUser(c echo.Context) error {
        id, err := strconv.Atoi(c.Param("id"))
        if err != nil {
                return echo.NewHTTPError(http.StatusBadRequest, "id должен быть числом")
        }

        // здесь был бы поиск в базе: users.Find(id)

        return c.JSON(http.StatusOK, map[string]any{
                "id":   id,
                "name": fmt.Sprintf("user-%d", id),
        })
}

// в main(): e.GET("/users/:id", getUser)

strconv.Atoi парсит строку в int и возвращает ошибку, если в строке не только цифры. После успешной конвертации id — настоящее число: его можно передавать в базу или сервисный слой. fmt.Sprintf здесь имитирует имя — в реальном проекте на этом месте стоит запрос к хранилищу. Нужны импорты strconv и fmt.

Два параметра в одном маршруте#

Параметров в пути может быть сколько угодно, если между ними есть статические разделители: /users/:uid/posts/:pid — корректный маршрут, где uid — автор, pid — конкретный пост. Так выражают вложенность ресурсов: все посты пользователя и один пост в отдельности. Правило вкуса, которое я советую: не ставьте два «голых» параметра подряд без статики — /users/42/7 не говорит клиенту, что 42 — пользователь, а 7 — пост, и читать такой код через полгода тяжело. Ещё одна ловушка — опечатка в имени: если в c.Param("uid") вы напишете uids, ошибки компиляции не будет, паники не будет — вернётся пустая строка. Поэтому параметры называют одинаково в маршруте и в коде, а пустое значение из c.Param — первый подозреваемый, когда ответ приходит без ожидаемых данных.

Query-параметры: c.QueryParam#

Всё после знака вопроса — строка запроса: /search?q=echo&page=2&tag=go&tag=web. Доступ в Echo двоякий: c.QueryParam("q") возвращает значение одного ключа, c.QueryParams() — url.Values со всеми парами (url.Values — это map[string][]string: один ключ может нести несколько значений, как tag выше; первое значение достаётся через .Get("tag")). Поведение при отсутствии параметра простое и заодно коварное: c.QueryParam вернёт пустую строку — без ошибки, без nil. В Gin есть DefaultQuery, подставляющий значение по умолчанию; в Echo такого аналога нет — дефолты подставляете сами, обычно через fallback после неудачной конвертации в число. Это не «недоработка», а последовательность минимализма, но помнить о ней надо: запрос без ?page= — нормальная ситуация, а не исключение.

Пагинация: page и limit с проверкой

GO
func search(c echo.Context) error {
        q := c.QueryParam("q")

        page, err := strconv.Atoi(c.QueryParam("page"))
        if err != nil || page < 1 {
                page = 1
        }

        limit, err := strconv.Atoi(c.QueryParam("limit"))
        if err != nil || limit < 1 || limit > 100 {
                limit = 20
        }

        return c.JSON(http.StatusOK, map[string]any{
                "query": q,
                "page":  page,
                "limit": limit,
        })
}

// в main(): e.GET("/search", search)

Пустая строка вместо page — Atoi вернёт ошибку, и сработает fallback: page = 1. Верхняя граница limit — не паранойя: ?limit=1000000 от случайного клиента не должен превращаться в запрос, который уронит базу. Числовые query-параметры без дефолтов и границ — типовая дыра в новых API.

Что вернёт search без параметров

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

// GET /search  (запрос без ?page= и ?limit=)

q := c.QueryParam("q")
page, err := strconv.Atoi(c.QueryParam("page"))
if err != nil || page < 1 {
        page = 1
}

Wildcard и приоритет: кто победит#

Звёздочка в конце пути — wildcard: /files/ матчит хвост любой длины, включая вложенные подпути, а значение достаётся c.Param(""). Классическое применение — раздача файлов: /files/docs/report.pdf вернёт вам docs/report.pdf одним куском. Теперь тот самый запутывающий момент: пусть объявлены /users/new и /users/:id. Запрос GET /users/new подходит под оба маршрута — и Echo выберет статический /users/new, без исключений и без учёта порядка регистрации. Логика простая: параметр — это «любое значение», статика — «именно это», поэтому конкретное правило всегда сильнее. А если настоящих /users/new не существует, а запрос пришёл? Тогда id будет строкой "new", и уже ваш код решит, что с ней делать: после неудачного Atoi — честная ошибка 400. Отдельно запомните: /users и /users/ — разные маршруты; склейкой trailing slash занимается middleware.RemoveTrailingSlash() через e.Pre — вернёмся к нему в уроке про middleware.

Виды маршрутов: шпаргалка#

МаршрутМатчитсяКак достать значение
GET /healthточный путь /healthпараметров нет
GET /users/:idодин сегмент после /users/c.Param("id")
GET /users/:uid/posts/:pidдва сегмента на своих позицияхc.Param("uid"), c.Param("pid")
GET /files/*хвост любой длины, включая вложенныеc.Param("*")
GET /searchпуть /search, остальное — после знака ?c.QueryParam("q"), c.QueryParams()

Проверка: приоритет маршрутов

Объявлены e.GET("/users/:id", a) и e.GET("/users/new", b). Что ответит Echo на GET /users/new?

Заполните пропуски в getUser

Восстановите обработчик: метод регистрации, конвертация, чтение параметра и статус для мусорного запроса.

e.("/users/:id", getUser) func getUser(c echo.Context) error { id, err := strconv.(c.("id")) if err != nil { return echo.NewHTTPError(http.Status, "id должен быть числом") } return c.JSON(http.StatusOK, map[string]any{"id": id}) }

Соберите обработчик search из строк

Расставьте строки обработчика в правильном порядке — от чтения query до возврата JSON.

Строки

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

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

Карточки: маршруты и параметры

Термин

:id в маршруте

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

Объявление path-параметра: матчит один сегмент пути. Значение достаётся по имени через c.Param("id").

1 / 6

Что дальше по курсу#

Маршруты описаны, параметры достаются — следующий логичный шаг: работа с телом запроса и заголовками. Это урок про запросы и ответы: c.Bind, c.JSON, NoContent, Redirect. После — привязка и валидация со struct-тегами и кастомным validator, где ручные проверки вроде Atoi переезжают в декларативные правила. Для статус-кодов и пагинации как практик загляните в урок о RESTful-практиках: половина материала уже знакома вам по этому уроку. А префиксы /api/v1, общие middleware на поддеревьях и версионирование — в уроке про группы.

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

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

Вывод

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

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

  • Path-параметры объявляются двоеточием (/users/:id) и достаются c.Param("id"): значение всегда строка.
  • Числовой параметр — strconv.Atoi плюс обработка ошибки: мусор в URL — это 400, а не 500.
  • Query-параметры — c.QueryParam (один) и c.QueryParams (url.Values для повторяющихся ключей).
  • Дефолтов для query в Echo нет: fallback после Atoi — рабочий шаблон (page = 1, limit = 20).
  • Wildcard /files/* матчит хвост пути, значение достаётся c.Param("*").
  • Приоритет: статика > параметр > wildcard; порядок объявления маршрутов не важен.
  • /users и /users/ — разные маршруты; склейка — middleware.RemoveTrailingSlash через e.Pre.

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

Добавьте в проект из прошлого урока два маршрута: GET /books/:id (с Atoi и 400 на мусор) и GET /books/year/:year (тоже число). Убедитесь, что запрос /books/year/2001 попадает в year-маршрут, а не в :id, и письменно объясните себе почему. Затем допишите верхнюю границу 50 для limit в обработчике /search и проверьте curl-ом: запрос ?limit=1000 должен вернуть limit = 50.

Личные заметкиПусто
Хранятся только в вашем браузере (Echo · Маршрутизация в Echo: параметры пути, query и приоритеты)

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

Как достать path-параметр в Echo?

c.Param("id"): имя в скобках — то, что вы объявили в маршруте после двоеточия. Значение всегда строка; для числа — strconv.Atoi с обработкой ошибки и ответом 400.

Что вернёт c.QueryParam, если параметра нет?

Пустую строку, без ошибки. Дефолтного метода вроде gin-овского DefaultQuery в Echo нет — подставляйте значение сами после неудачной конвертации.

Что важнее: статический маршрут или параметр?

Статика. Если под запрос подходят /users/new и /users/:id, Echo выберет /users/new. Это правило radix-дерева, а не порядок регистрации.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

GORM: ORM-библиотека для Go

Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.

  • Подключать БД и настраивать пул соединений
  • Описывать модели и связи: 1:1, 1:N, M:N
  • Писать CRUD и сложные выборки chainable-стилем
  • Контролировать N+1 через Preload