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

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

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

Урок 4 из 25 ~18 мин +50 XP

URLParam: параметры пути и query-строка

В хендлер пришёл адрес /notes/42, а работать нужно с самой заметкой — значит, из пути достаётся 42. chi.URLParam читает значения из фигурных скобок маршрута: параметры пути в go всегда строки, и на их границах живут битые числа и молчаливые опечатки. Разберём URLParam, query-строку и разницу между /notes/42 и /notes?page=2.

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

REST-каркас из прошлого урока обещал карточку заметки по GET /notes/{id} — теперь нужна механика: как хендлер узнаёт, какая именно заметка запрошена. Адрес приходит строкой /notes/42, и где-то в нём спрятан id. Роутер chi разбирает его ещё до вашего хендлера: сегмент {id} в шаблоне маршрута говорит «здесь будет произвольное значение», роутер сопоставляет фактический путь с шаблоном и складывает совпадения в контекст запроса. Достать значение оттуда — одна функция, chi.URLParam. Звучит просто, и это правда просто; сложности начинаются на границах. Параметр — всегда строка, значит рядом нужен strconv.Atoi и обработка ошибки. Имя в URLParam должно совпасть с именем в шаблоне байт в байт — иначе вернётся пустая строка: не exception, не ошибка компиляции, а молчаливая пустота, которая взорвётся дальше по цепочке. А ещё есть query-строка, которая выглядит похожей, но живёт по другим правилам и достаётся другими средствами. Этим границам и посвящён урок.

Параметры пути в chi: /notes/{id}#

Шаблон маршрута пишется фигурными скобками: r.Get("/notes/{id}", getNote). Имя внутри скобок — произвольный идентификатор: {id}, {noteID}, {slug} — это метка сегмента, а не переменная Go. Когда приходит запрос GET /notes/42, chi сопоставляет путь с шаблоном сегмент за сегментом, находит несовпадающую позицию и запоминает: id равно 42. Хендлер достаёт значение функцией chi.URLParam(r, "id") из контекста запроса, куда роутер положил разбор. Два жёстких правила. Первое: {id} ловит ровно один сегмент — до следующего слэша; /notes/42/comments в этот маршрут не влезет, для хвостов произвольной длины есть маршруты со звёздочкой, о них дальше по курсу. Второе: имя в chi.URLParam обязано совпасть с именем в шаблоне — не по смыслу, а по буквам: "ID" и "id" — разные метки. Chi не валидирует имена заранее и не ругается на незнакомое: вернёт пустую строку, о последствиях — отдельным блоком ниже. В одном маршруте параметров может быть несколько: /users/{userID}/notes/{noteID} — легальный шаблон с двумя метками.

Достаём id из пути

GO
r.Get("/notes/{id}", getNote)

func getNote(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")

        w.Write([]byte("запрошена заметка " + id))
}

Запрос GET /notes/42 напечатает «запрошена заметка 42», GET /notes/7 — про семёрку. Хендлер один на все заметки, различие лежит в параметре — в этом сила шаблонов: пятьдесят заметок не превращаются в пятьдесят регистраций. Значение достаётся ровно по имени метки из шаблона: URLParam(r, "id") соответствует {id}. Пока id используется как строка, код безупречен; проблемы начинаются, когда из строки делают число, — об этом следующий блок.

Параметры — всегда строки: strconv.Atoi#

URLParam возвращает string, всегда, без исключений. В пути нет типов: 42 — это символы «4» и «2», и пока вы только вставляете их в ответ или лог, строка устраивает. Но база ждёт int64, арифметика сравнения версий ждёт int — значит, нужен разбор: strconv.Atoi переводит строку в int и честно возвращает ошибку. Ошибка здесь не украшение: пользователь вольен вписать в адрес что угодно — /notes/hello, /notes/99999999999999999999, /notes/-5 — и все три случая Atoi отвергнет: первый не число, второй не влезает в int, третий синтаксически валиден, но пусть бизнес-логика решает сама. Правило каркаса: разбор параметра — первая строка хендлера, и на ошибке хендлер отвечает 400 Bad Request с внятным текстом, а не падает и не лезет в базу с мусором. Код ответа важен: 400 говорит «ошибка на стороне клиента», и пейджер мониторинга не будет дёргать дежурных из-за опечатки пользователя в адресе.

Число с проверкой

GO
func getNote(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")

        n, err := strconv.Atoi(id)
        if err != nil {
                http.Error(w, "id должен быть числом", http.StatusBadRequest)
                return
        }

        // n — int, дальше работаем с числом
        fmt.Fprintf(w, "заметка %d", n)
}

Atoi возвращает пару: число и ошибку. Ошибка проверяется до любого использования n — мусор не проходит дальше в сервисный слой. http.Error ставит статус и печатает текст одной строкой: для прототипа достаточно; когда появится единый JSON-формат ошибок, это место заменит его, а проверка останется на том же месте. strconv и fmt — импорты стандартной библиотеки, chi тут уже ни при чём.

Путь или query: /notes/42 против /notes?page=2#

Параметры пути и query-строка выглядят похоже — и то, и другое «данные в адресе», — но назначения у них разные, и путаница между ними портит API быстрее, чем любые опечатки. Параметр пути определяет, какой ресурс запрошен: /notes/42 — конкретная заметка, её идентичность, и другой заметки по этому адресу быть не может. Query-строка фильтрует или оформляет представление коллекции: /notes?page=2&tag=go — всё та же страница «списка заметок», просто в другом разрезе. Практический тест для дизайна API: если убрать параметр и адрес перестаёт указывать на конкретный ресурс — это путь; если адрес всё равно ведёт на тот же список — это query. Закладки, кэши и журналы читаются легче, когда ресурсы живут в пути, а настройки вида — после знака вопроса. Разборы в chi тоже разные: URLParam достаёт метки шаблона, а query читается средствами net/http из r.URL — роутер тут не нужен, об этом ниже.

Параметр пути или query-строка#

КритерийПараметр пути: /notes/42Query: /notes?page=2
Рольидентификатор ресурсафильтр или оформление представления
Обязательностьбез него адрес не указывает на ресурсможно убрать — смысл адреса сохранится
Достаётся в chichi.URLParam(r, "id")r.URL.Query().Get("page")
Типичные примеры{id}, {slug}, {userID}page, limit, sort, q
Кэши и закладкиадрес ресурса стабиленварианты одного и того же ресурса

Проверка: где место параметра

Нужно отсортировать список заметок по дате: какой дизайн адреса корректен?

Query-строка: r.URL.Query()#

Для query-строки в chi нет собственной функции — и правильно: query разбирает стандартная библиотека, адрес запроса и так принадлежит net/http. r.URL.Query() возвращает map[string][]string — ключи и значения, разложенные по знакам & и =; значения — срез, потому что один ключ может повторяться. На практике почти всегда берут метод Get: r.URL.Query().Get("page") возвращает первое значение или пустую строку, если ключа нет, — то же молчаливое поведение, что у URLParam, и те же проверки нужны следом. Разбор query нужен ради дефолтов: пустая page — значит 1, пустой limit — значит 20. Из строки снова делают число той же strconv.Atoi с той же обработкой ошибки. Отдельно запомните: URLParam на query-ключи не работает никогда — метки живут только в шаблоне пути, и chi.URLParam(r, "page") вернёт пустоту, даже если адрес содержит ?page=2. Промах не репортится — это самая коварная разновидность опечатки имени.

Пагинация через query

GO
r.Get("/notes", listNotes)

func listNotes(w http.ResponseWriter, r *http.Request) {
        page, err := strconv.Atoi(r.URL.Query().Get("page"))
        if err != nil || page < 1 {
                page = 1
        }

        fmt.Fprintf(w, "список заметок, страница %d", page)
}

Отсутствующий или битый page здесь не ошибка, а повод для дефолта: список без параметров показывает первую страницу, и 400 здесь только мешал бы. Поэтому err не отдаётся наружу, а сворачивается в page 1. Отрицательные значения ловит вторая половина условия — query-параметры заслуживают проверки так же, как параметры пути.

Проверка: вложенные параметры

Зарегистрирован r.Get("/users/{userID}/notes/{noteID}", h); пришёл запрос GET /users/7/notes/3. Что вернёт chi.URLParam(r, "userID")?

Вложенные параметры и вложенные ресурсы#

Шаблон /users/{userID}/notes/{noteID} описывает классическую вложенность: заметки конкретного пользователя. Имена обязаны различаться — два {id} в одном маршруте превратили бы разбор в лотерею, — поэтому метки зовут по смыслу: userID и noteID. В хендлере обе метки достаются парой вызовов chi.URLParam, а логика «заметка 3 принадлежит пользователю 7» проверяется в сервисном слое: если заметка у другого владельца, это 404, а не 200 с чужими данными. Вложенность — удобный приём, но не бесплатный: цепочка /users/7/notes/3/comments/9 растёт в глубину быстрее, чем в полезность, и на четвёртом уровне шаблон проще развернуть в отдельный ресурс. Для сравнения: в Gin те же метки пишутся двоеточием — /users/:userID — и достаются функцией c.Param; механика совпадает вплоть до опечаток, различается синтаксис. Тот же разбор на другом фреймворке — в уроке Gin про параметры.

Что напечатает хендлер?

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

r.Get("/users/{userID}/notes/{noteID}", func(w http.ResponseWriter, r *http.Request) {
        u := chi.URLParam(r, "userID")
        n := chi.URLParam(r, "noteID")

        fmt.Fprintf(w, "user=%s note=%s", u, n)
})

// запрос: GET /users/7/notes/3

Соберите разбор id

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

r.Get("/notes/", getNote) func getNote(w http.ResponseWriter, r *http.Request) { id := chi.(r, "id") n, err := strconv.(id) if err != nil { http.Error(w, "битый id", http.) return } fmt.Fprintf(w, "заметка %d", n) }

Правила работы с параметрами#

  • Имя в шаблоне и в chi.URLParam совпадает байт в байт: "id" и "ID" — разные метки; имя выносится в константу.
  • Параметр — всегда string: перед арифметикой нужен strconv.Atoi с проверкой ошибки.
  • Ошибка разбора id — 400 Bad Request, а не 500: в кривом адресе виноват клиент.
  • Query-параметры достаются r.URL.Query().Get и через URLParam не видны никогда.
  • В одном маршруте — несколько меток с уникальными именами: /users/{userID}/notes/{noteID}.

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

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

Строки

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

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

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

Термин

chi.URLParam(r, "id")

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

Достаёт значение сегмента {id} из шаблона маршрута по имени метки. Возвращает string; при несовпадении имени — пустую строку молча.

1 / 6

Параметры пути разобраны, query читается с дефолтами — каркас /notes научился работать с конкретной заметкой, а не только с коллекцией. Следующая ступень — middleware: лог, восстановление после паники и счётчики навешиваются на цепочку одним r.Use, и хендлеры перестают заниматься служебной работой. А если однажды {id} станет тесным — понадобится забрать весь хвост пути, например имя файла с вложенными папками, — это тема урока про wildcards: звёздочка ловит остаток пути целиком. Для практики соберите каркас из прошлого урока и хендлеры из этого в один файл: локальный Go запустит его как есть, и curl-ом видно всё — и карточку по /notes/42, и 400 на /notes/hello.

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

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

Вывод

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

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

  • Шаблон {id} ловит один сегмент пути; значение достаётся chi.URLParam(r, "id") ровно по имени метки.
  • Параметры — всегда строки: числа разбираются strconv.Atoi с обработкой ошибки и ответом 400 на мусор.
  • Несовпадение имени в шаблоне и в URLParam возвращает пустую строку молча — имя параметра выносится в общую константу.
  • Query-строка не видна через URLParam: r.URL.Query().Get("page") из net/http, с дефолтами и проверкой.
  • Путь идентифицирует ресурс (/notes/42), query оформляет представление (/notes?page=2) — от этого зависят кэши, закладки и логи.
  • Вложенный маршрут /users/{userID}/notes/{noteID} разбирается по уникальным именам меток.
  • Владение вложенным ресурсом проверяется в сервисном слое: чужая заметка — 404, а не утечка данных.

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

Доработайте каркас заметок: в getNote разберите id через strconv.Atoi и отвечайте 400 на нечисловой путь, а в listNotes добавьте query-параметры page и limit с дефолтами 1 и 20. Проверьте curl-ом адреса /notes/abc, /notes/-3 и /notes?page=2&limit=5 и убедитесь, что ответы совпадают с планом.

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

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

Что вернёт chi.URLParam, если имени нет в маршруте?

Пустую строку — без ошибки и без паники: «метки нет» и «метка есть, но пуста» для chi неразличимы. Поэтому имя берут из общей константы для шаблона и вызова, а пустоту после URLParam проверяют так же серьёзно, как ошибку strconv.Atoi.

Почему URLParam не читает ?page=2?

Метки шаблона живут только в пути: chi сопоставляет сегменты до слэшей, и query-строка в маршрутизацию не входит. Query разбирает стандартная библиотека — r.URL.Query().Get("page"). Перепутать их проще всего в спешке: промах выглядит как пустая строка, а не как ошибка.

Как достать несколько параметров из одного маршрута?

Дать меткам уникальные имена: /users/{userID}/notes/{noteID}, затем два вызова chi.URLParam — по одному на метку. Имена должны различаться, потому что разбор идёт по позициям сегментов, а достаётся каждая пара по строковому имени из контекста запроса.

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

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

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

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

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

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

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