---
title: "Параметры пути и запроса"
description: "Параметры в Gin: c.Param для путей /users/:id, c.Query и DefaultQuery для query-строки, QueryArray для повторов и wildcard *filepath. Примеры кода с разбором."
course: "Gin: самый популярный веб-фреймворк Go"
url: "https://golangmaster.ru/courses/gin/lessons/03-parametry/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Параметры пути и запроса

*Курс «Gin: самый популярный веб-фреймворк Go», урок 3 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/gin/lessons/03-parametry/*

REST-маршрут без параметров почти бесполезен: нам нужны /users/42 и /search?q=go&page=2. Разберём оба способа Gin — параметры пути через c.Param и query-параметры через c.Query, DefaultQuery и QueryArray — на реальных хендлерах, вместе с самыми коварными ловушками: пустыми значениями и ручной конвертацией типов.

### Два вида параметров и где какой живёт

Параметры приходят в двух местах. Параметры пути (/users/42) идентифицируют ресурс: 42 — это конкретный пользователь, без него маршрут не тот. Query-параметры (/search?q=go&page=2) уточняют запрос: фильтры, сортировка, пагинация, поиск. Это не выдумка Gin, а конвенция REST, и она осмысленная: путь задаёт «что», query — «как именно». Практическое следствие для дизайна API: значение обязательное и определяющее сущность — в путь; опциональное и его можно не указывать — в query. Пытаться пихать фильтры в путь (/users/active/vip) — путь к комбинаторному взрыву: каждая новая опция удваивает число эндпоинтов. И наоборот, идентификатор в query выглядит небрежно: /user?id=42 работает, но читается хуже и ломает иерархию ресурсов.

### Параметры пути: c.Param и синтаксис :id

В маршруте Gin сегмент, начинающийся с двоеточия, становится параметром: r.GET("/users/:id", ...) примет и /users/42, и /users/ann. Значение читается в хендлере через c.Param("id") — ключ без двоеточия. Важно: тип всегда string. Если нужен int — конвертируйте сами через strconv.Atoi с обработкой ошибки; никакого автомата у Gin нет, и это правильно: решить, что делать с мусором, может только бизнес-логика. Параметров может быть несколько: /users/:id/posts/:post_id — оба достанутся через Param. Один параметр матчит ровно один сегмент: /users/ann/posts/7 — да, а /users/a/b/5 — нет, потому что после /users там два сегмента вместо одного.

### Маршрут с двумя параметрами

```go
   r.GET("/users/:id/posts/:post_id", func(c *gin.Context) {
                id := c.Param("id")
                postID := c.Param("post_id")

                c.JSON(http.StatusOK, gin.H{
                        "user_id": id,
                        "post_id": postID,
                })
        })
```

Оба параметра объявлены в пути через двоеточие и прочитаны по имени. GET /users/7/posts/42 вернёт {"user_id":"7","post_id":"42"} — обе строки, даже если выглядят как числа. Попытка достать несуществующий параметр, например c.Param("slug"), не паникует: вернётся пустая строка — проверяйте имена внимательно.

### Wildcard *filepath и правила роутера

Звёздочка ловит остаток пути целиком: r.GET("/static/*filepath") обслужит /static/css/main.css, а c.Param("filepath") вернёт "/css/main.css" — с ведущим слешем. Wildcard обязан быть последним сегментом маршрута: два wildcards в одном пути или параметр после wildcard — ошибка при регистрации, роутер запаникует сразу при старте. Это удобно: опечатка в дереве маршрутов ловится на первом же go run, а не на проде. Старые версии Gin запрещали смешивать статические сегменты и параметры на одном уровне: /users/:id и /users/new конфликтовали паникой при регистрации. Современный роутер такие пары принимает и отдаёт приоритет статике: /users/new попадёт в точный маршрут, /users/42 — в :id. Полагайтесь на это осознанно: чем больше пересечений, тем непонятнее таблица маршрутов в логах при старте. Для проверки руками хватит c.String(http.StatusOK, "файл: %s", c.Param("filepath")) — так можно без JSON убедиться, что хвост пути приходит целиком.

> **Параметр — всегда строка.** c.Param и c.Query возвращают string, даже если клиент прислал цифры. Прежде чем использовать id := c.Param("id") как число, прогоните через strconv.Atoi и обработайте ошибку: иначе мусор вроде /users/привет уедет в SQL-запрос или индекс массива в неожиданном виде. Молчаливое игнорирование ошибки Atoi (id, _ := strconv.Atoi(...)) превращает мусор в ноль — ловите её явно.

### Параметры и SQL: почему нельзя доверять строкам из URL

Отдельно про безопасность, потому что параметры пути и запроса — любимая дверь для атак. Типичный баг новичка: собрать SQL конкатенацией — query := "SELECT ... WHERE id = " + c.Param("id"). Клиент не обязан присылать число: он пришлёт /users/1 OR 1=1, и вместо одного пользователя запрос вернёт всю таблицу, а если такой же паттерн сидит в DELETE — удалит все строки. Правило одно: пользовательские данные едут в SQL только через плейсхолдеры ($1 или ?) в параметризованных запросах — sqlx, sqlc, GORM и database/sql все умеют передавать аргументы отдельно от текста запроса. То же касается формирования имён файлов из wildcard и редиректов из query-параметра: что бы ни пришло по HTTP, это данные, а не команды, и относиться к ним стоит соответственно.

### Query-параметры: c.Query, DefaultQuery, QueryArray

Query-строку Gin разбирает за вас. c.Query("q") возвращает значение или пустую строку, если параметра нет — и тут первая ловушка: «нет значения» и «?q=» неотличимы. c.DefaultQuery("page", "1") подставляет значение по умолчанию, но только когда параметра нет вовсе; при ?page= вернётся пустая строка, а не «1» — если разница важна, берите GetQuery с булевым ok. Повторяющиеся ключи (?tag=go&tag=web) собирает c.QueryArray("tag") — вернётся слайс строк, а при отсутствии ключа — пустой слайс. И помните про типы: у query их нет, всё строковое, числа — опять strconv. Для реальной раздачи статики путь с wildcards закрывают r.Static и StaticFS — им посвящён [отдельный урок](/courses/gin/lessons/11-statika/).

### Методы чтения параметров в Gin

| Метод | Откуда | Если значения нет | Тип |
| --- | --- | --- | --- |
| c.Param("id") | путь /users/:id | пустая строка | string |
| c.Query("q") | query-строка | пустая строка | string |
| c.DefaultQuery("page", "1") | query-строка | дефолт; но при ?page= — пустая строка | string |
| c.QueryArray("tag") | повторы ?tag=a&tag=b | пустой слайс | []string |
| c.GetQuery("q") | query-строка | пустая строка и ok=false | (string, bool) |

### Хендлер /search с тремя источниками данных

```go
   r.GET("/search", func(c *gin.Context) {
                q := c.Query("q")

                page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
                if err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "page must be a number"})
                        return
                }

                tags := c.QueryArray("tag")

                c.JSON(http.StatusOK, gin.H{"q": q, "page": page, "tags": tags})
        })
```

Три источника в одном хендлере: обязательный q без проверки отсутствия (пустой поиск — валидный бизнес-кейс), page с дефолтом «1» и строгой проверкой числа, и tags — повторяющиеся ключи. Обратите внимание: при отсутствии tags JSON вернёт "tags": [] — пустой массив, а не null; фронтенду это удобно. Atoi на нечисловом значении вернёт ошибку, и мы честно отдаём 400 вместо молчаливого нуля. Проверьте это локально: curl -i покажет статус и заголовки, а строка [GIN] в консоли повторит запрошенный путь с параметрами — так удобно сверять ожидания с реальностью.

### Собирать поля руками или через binding?

Ручное чтение Param/Query — нормально для одного-двух полей. Когда полей становится пять и больше, появляется соблазн связать всё со структурой за один вызов — это ShouldBindQuery и ShouldBindUri: теги form:"page" и uri:"id" плюс автоматическая проверка обязательных полей и типов. Заодно уходят ручные Atoi: binding конвертирует типы по полям структуры и вернёт внятную ошибку, если page — не число. Разбирать запрос вручную стоит, когда поле одно или логика нестандартная; иначе — binding, о нём [следующий урок](/courses/gin/lessons/04-binding/). И помните: всё, что пришло снаружи, валидируем — теги binding и validate закрывают большинство проверок до бизнес-логики, о чём подробно в уроке про [валидацию](/courses/gin/lessons/09-validaciya/).

### Отладка: когда параметр «не приходит»

Самая частая жалоба новичка: c.Param возвращает пустую строку, хотя значение вроде есть. В девяти случаях из десяти маршрут зарегистрирован не так, как запрошен: /users/:id не матчит /users/ann/posts/7, а опечатка в имени — c.Param("userId") при маршруте :id — тихо даёт пустую строку. Инструмент уже у вас в консоли: при старте Gin в debug-режиме печатает все зарегистрированные маршруты — [GIN-debug] GET /users/:id --> ... — сверьте свой запрос с этим списком, путь и метод должны совпасть посимвольно. Если маршрут есть, а значения нет, значит вы читаете не тот источник: Param вместо Query или наоборот. И не забывайте про редиректы trailing slash: curl без флага -L покажет 301, а браузер молча пойдёт дальше — внешне это выглядит как «то работает, то нет».

### Что вернёт GET /search?page= ?

```go
   r.GET("/search", func(c *gin.Context) {
                page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
                if err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": "page must be a number"})
                        return
                }
                c.JSON(http.StatusOK, gin.H{"page": page})
        })
```

**Что выведет программа?**

- ✅ 400 и {"error":"page must be a number"}
- 200 и {"page":"1"}
- 200 и {"page":1}
- 404 page not found

Ключ page существует (пусть и с пустым значением), поэтому DefaultQuery вернёт "", а не "1". strconv.Atoi("") даёт ошибку — хендлер честно отвечает 400. Это самая коварная ловушка query-параметров.

### Проверка: Param против Query

**Вопрос:** Маршрут r.GET("/posts/:id/comments"), запрос GET /posts/7/comments?author=ann. Где брать 7 и ann?

- ✅ 7 — c.Param("id"), ann — c.Query("author")
- оба через c.Param: c.Param("id") и c.Param("author")
- оба через c.Query: c.Query("id") и c.Query("author")
- 7 — c.Query("id"), ann — c.Param("author")

7 сидит в пути — значит Param; ann стоит после знака вопроса — значит Query. Путаница Param/Query — источник половины 404 у новичков.

### Допишите хендлер GET /users/:id

Вставьте метод регистрации маршрута, чтение параметра пути и конвертацию в число.

```go
r.___("/users/:id", func(c *gin.Context) {
        id := c.___("id")

        n, err := strconv.___(id)
        if err != nil {
                c.JSON(http.___, gin.H{"error": "bad id"})
                return
        }

        c.JSON(http.StatusOK, gin.H{"id": n})
})
```

Подсказка: Параметр пути читается методом Param; число из строки — strconv.Atoi; статус 400 — http.StatusBadRequest.

Правильные ответы в пропусках: `GET`, `Param`, `Atoi`, `StatusBadRequest`

### Соберите хендлер /users/:id из строк

Расставьте строки: чтение параметра, конвертация, обработка ошибки, ответ.

Правильный порядок строк:

1. `func getUser(c *gin.Context) {`
2. `  id := c.Param("id")`
3. `  n, err := strconv.Atoi(id)`
4. `  if err != nil {`
5. `    c.JSON(http.StatusBadRequest, gin.H{"error": "bad id"})`
6. `    return`
7. `  }`
8. `  c.JSON(http.StatusOK, gin.H{"id": n})`
9. `}`

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

- **:id в маршруте** — Параметр одного сегмента: /users/:id матчит /users/42. Читается c.Param("id"), тип — string.
- ***filepath** — Wildcard: ловит остаток пути вместе со слешами. Обязан быть последним сегментом маршрута.
- **c.Param** — Чтение параметра пути по имени без двоеточия. Нет значения — пустая строка, не ошибка.
- **c.Query** — Чтение query-параметра. Нет ключа или он пуст (?q=) — пустая строка.
- **c.DefaultQuery** — Query с дефолтом, когда ключа нет вовсе. При ?page= вернёт пустую строку, а не дефолт.
- **c.QueryArray** — Слайс значений повторяющихся ключей: ?tag=go&tag=web → [go web]. Нет ключа — пустой слайс.

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

- Идентификаторы ресурсов — в пути (:id), фильтры и пагинация — в query.
- c.Param("id") всегда string; числа конвертирует strconv с обработкой ошибки.
- Wildcard *filepath — последний сегмент маршрута, возвращает хвост пути с ведущим слешем.
- c.Query без ключа даёт ""; DefaultQuery подставляет дефолт, но не при пустом значении ?page=.
- Повторяющиеся ключи собирает c.QueryArray; отсутствующий ключ даёт пустой слайс.
- Пять и больше полей — переходите на ShouldBindQuery/ShouldBindUri: конвертация и валидация за один вызов.

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

Расширьте хендлер /search: добавьте параметр limit через DefaultQuery с дефолтом «10» и проверкой Atoi. Затем добавьте маршрут GET /files/*path, который отвечает JSON {"path": ...} — убедитесь через curl, что ведущий слеш в параметре есть. Проверьте запросы: /search?q=go&tag=web&tag=perf&page=2, /search?page=abc и /files/a/b.txt.

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

### Чем c.Param отличается от c.Query?

Param читает сегменты пути, объявленные через :id или *wildcard; Query — значения после знака вопроса. Param — обязательная часть маршрута, Query — опциональные уточнения.

### Как получить page сразу числом?

Руками: strconv.Atoi(c.Query("page")) с обработкой ошибки. Автоматически: ShouldBindQuery со структурой, где поле имеет тип int, — конвертация и валидация произойдут за один вызов.

### Почему /users/ даёт 404, если маршрут /users/:id?

Пустой сегмент не матчится с :id. Для списка пользователей зарегистрируйте отдельный маршрут r.GET("/users", ...). Gin умеет редиректить trailing slash (RedirectTrailingSlash по умолчанию), но только между существующими маршрутами.
