Любой 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
GOe.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: параметр, конвертация, ответ
GOfunc 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 с проверкой
GOfunc 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
Восстановите обработчик: метод регистрации, конвертация, чтение параметра и статус для мусорного запроса.
Соберите обработчик search из строк
Расставьте строки обработчика в правильном порядке — от чтения query до возврата JSON.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: маршруты и параметры
Что дальше по курсу#
Маршруты описаны, параметры достаются — следующий логичный шаг: работа с телом запроса и заголовками. Это урок про запросы и ответы: 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.
Личные заметкиПусто
Вопросы и ответы по уроку
Как достать path-параметр в Echo?
c.Param("id"): имя в скобках — то, что вы объявили в маршруте после двоеточия. Значение всегда строка; для числа — strconv.Atoi с обработкой ошибки и ответом 400.
Что вернёт c.QueryParam, если параметра нет?
Пустую строку, без ошибки. Дефолтного метода вроде gin-овского DefaultQuery в Echo нет — подставляйте значение сами после неудачной конвертации.
Что важнее: статический маршрут или параметр?
Статика. Если под запрос подходят /users/new и /users/:id, Echo выберет /users/new. Это правило radix-дерева, а не порядок регистрации.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
GORM: ORM-библиотека для Go
Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.
- Подключать БД и настраивать пул соединений
- Описывать модели и связи: 1:1, 1:N, M:N
- Писать CRUD и сложные выборки chainable-стилем
- Контролировать N+1 через Preload