JavaRush /Курси /Go SELF /Читання query-параметрів на сервері — r.URL.Query()

Читання query-параметрів на сервері — r.URL.Query()

Go SELF
Рівень 57 , Лекція 2
Відкрита

1. Де в net/http «живе» URL запиту

Коли ви вперше пишете обробник (handler) у Go, здається, що вам вручили два подарунки: http.ResponseWriter і *http.Request. Перший — щоб формувати відповідь, другий — щоб розуміти, на що саме відповідати. А в другому, окрім методу (GET/POST) і заголовків, є ще й URL запиту — уже розкладений у структуру. Це не магія, а просто нормальний інтерфейс сервера. До речі, http.Request ще й несе Context, щоб запит можна було коректно скасувати, але в цій лекції ми лише зауважимо цей факт і не будемо заглиблюватися.

Найпростіший «скелет» сервера виглядає приблизно так. Він нагадує класичні приклади net/http.


package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/api/v1/tasks", listTasksHandler)
	_ = http.ListenAndServe(":8080", nil)
}

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok") // ok
}

Поки що обробник нічого не читає, але головне вже є: r.URL — це *url.URL, і саме там лежать Path, RawQuery та все, що потрібно для query-параметрів.

2. r.URL — це вже *url.URL, а не «рядок, який треба парсити»

Дуже поширена помилка новачка — думати, що в HTTP-запиті URL зберігається лише як рядок і що сервер не дає вам справжньої структури. Насправді net/http уже зробив за вас частину роботи: r.URL — це вказівник на структуру url.URL. Там окремо зберігаються шлях, query-рядок і навіть фрагмент. Щоправда, фрагмент до сервера зазвичай не доходить, бо браузер його не надсилає, — але це вже деталі.

Давайте просто подивимося, що приходить в обробник, не намагаючись нічого обробляти. Це корисно для діагностики: іноді ви думаєте, що у вас limit=10, а насправді клієнт шле Limit=10, і ви годину шукаєте, чому нічого не працює. Програміст без діагностики — як кіт без коробки: ніби й живий, але тривожно.

package main

import (
	"fmt"
	"net/http"
)

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "path=%q\n", r.URL.Path)         // path="/api/v1/tasks"
	fmt.Fprintf(w, "rawQuery=%q\n", r.URL.RawQuery) // rawQuery="done=true&limit=10"
}

Зверніть увагу: RawQuery зберігається без символа ?. Тобто там буде "a=b&c=d", а не "?a=b&c=d".

3. Головна кнопка: r.URL.Query()url.Values

Ось той самий момент, заради якого ми зібралися. У url.URL query-рядок зберігається як сирий текст (RawQuery), але працювати з ним вручну незручно. Тому в url.URL є метод Query(), який повертає url.Values — структуру, заточену саме під query-параметри.

Важливо зрозуміти ідею: Query() перетворює «рядок формату a=b&c=d» на нормальну структуру даних «ключ → список значень». І це одразу пояснює, чому query-параметри не варто уявляти як map[string]string: один ключ може траплятися кілька разів (tag=go&tag=http), і це нормальний, стандартний випадок.

Мінімальний приклад читання:

package main

import (
	"fmt"
	"net/http"
)

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()

	fmt.Fprintln(w, "done:", q.Get("done"))   // done: true
	fmt.Fprintln(w, "limit:", q.Get("limit")) // limit: 10
}

Тут Get("done") поверне рядок. Навіть якщо ви очікуєте bool або int, на рівні HTTP це все одно рядок. Перетворювати на bool/int ми будемо пізніше; сьогодні наша мета — впевнено дістати значення.

4. Чому url.Values зберігає []string і чим Get відрізняється від q[...]

Нормально, якщо спочатку url.Values викликає легке здивування: «Чому там []string, якщо я хочу лише одне значення?!». Але це якраз той випадок, коли стандартна бібліотека не знущається з вас, а береже від майбутнього болю.

Розгляньмо класичний список тегів: /api/v1/tasks?tag=go&tag=http&tag=cli. Це не екзотика, а звичайний спосіб передати список значень у query. Тому url.Values зберігає усі значення за ключем.

Порівняймо два способи читання:

Що читаємо Як читаємо Що отримуємо Коли зручно
«Одне значення» (перше)
q.Get("limit")
string (або "", якщо ключа немає) для одиночних параметрів (limit, sort, q)
«Усі значення»
q["tag"]
[]string (або nil, якщо ключа немає) для списків (tag=...&tag=...)

Покажімо це кодом:

package main

import (
	"fmt"
	"net/http"
)

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()

	fmt.Fprintln(w, "first tag:", q.Get("tag")) // first tag: go
	fmt.Fprintln(w, "all tags:", q["tag"])      // all tags: [go http cli]
}

Get бере перше значення. Іноді саме це і потрібно. Але якщо за контрактом параметр повторюється, читати його треба як список.

5. Мініпрактика: читаємо фільтри, пошук і теги

Давайте зробимо обробник, який ніби готується до фільтрації задач, але поки що нічого не фільтрує. Він просто читає параметри й повертає їх у відповіді. Це виглядає іграшково, але насправді це чудовий спосіб перевірити контракт: ви в браузері або через curl змінюєте query-параметри й одразу бачите, як сервер це зрозумів.

Ми працюватимемо з нашим ресурсом /api/v1/tasks і типовими параметрами: done, q, tag, limit, offset, sort, order.

package main

import (
	"fmt"
	"net/http"
)

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()

	fmt.Fprintln(w, "done =", q.Get("done"))
	fmt.Fprintln(w, "q    =", q.Get("q"))
	fmt.Fprintln(w, "tags =", q["tag"])
}

Перевірка в голові виглядає так:

  • /api/v1/tasks?done=true&q=milk&tag=go&tag=http
  • сервер поверне:
    • done = true
    • q = milk
    • tags = [go http]

Поки що все — це рядки та зрізи рядків. Це нормально. Сьогодні ми не будуємо «розумний фільтр»; сьогодні ми вчимося коректно й передбачувано діставати вхідні дані.

6. Нюанс: «ключ відсутній» і «значення порожнє» — не одне й те саме

q.Get("k") повертає порожній рядок і коли ключа немає, і коли ключ є, але значення порожнє (k=). Іноді це неважливо, але іноді — критично для контракту та значень за замовчуванням.

Наприклад, q= може означати «користувач спеціально хоче шукати порожній рядок» (хоча це дивно), а відсутність q — «пошуку немає, повертаємо все». Щоб розрізняти ці випадки, можна перевіряти наявність ключа через прямий доступ до мапи.

package main

import (
	"fmt"
	"net/http"
)

func listTasksHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()

	_, hasQ := q["q"]
	fmt.Fprintln(w, "q present:", hasQ) // q present: true/false
	fmt.Fprintln(w, "q value:", q.Get("q"))
}

Якщо клієнт надішле /api/v1/tasks?q=, тоді hasQ буде true, а q.Get("q") поверне "".

Це не фішка заради фішки. Це нормальна база для значень за замовчуванням і валідації: дефолт зазвичай застосовується, коли параметр відсутній, а не коли він присутній, але порожній.

7. Корисні нюанси та інструменти навколо query

url.ParseQuery: коли ви не в обробнику, але вам усе ще потрібна query-логіка

Іноді query-рядок у вас є лише як текст, але ви не всередині HTTP-обробника. Наприклад, ви пишете тести для функції розбору параметрів або робите утиліту, яка приймає рядок a=b&c=d і перетворює його на url.Values.

Для таких випадків існує url.ParseQuery. Логіка та сама, що й у r.URL.Query(), тільки вхід тут — звичайний рядок.

package main

import (
	"fmt"
	"net/url"
)

func main() {
	q, _ := url.ParseQuery("tag=go&tag=http&q=milk")

	fmt.Println("q:", q.Get("q"))  // q: milk
	fmt.Println("tags:", q["tag"]) // tags: [go http]
}

Це зручно для тестування: можна не піднімати сервер, а перевіряти, що розбір параметрів поводиться очікувано.

Нюанс: Query() повертає копію (і це добре)

Є тонкий момент, який у реальному коді спливає частіше, ніж здається. Коли ви робите: q := r.URL.Query() ви отримуєте url.Values. Формально це мапа, і ви навіть можете викликати q.Set(...). Але в контексті вхідного запиту на сервері це майже ніколи не потрібно, а якщо ви це робите, то не слід сприймати ці зміни як «я змінив URL запиту».

У нашому серверному сценарії правило просте: query-параметри — це вхід, а не місце для мутацій. Ми читаємо їх, валідовуємо, застосовуємо до логіки — і все. Якщо ви почнете виправляти запит на ходу, швидко отримаєте код, у якому складно зрозуміти, що прийшло від клієнта, а що ви самі собі вигадали.

Якщо ж вам колись знадобиться зібрати URL (наприклад, для клієнта або редиректу) — це робиться через url.URL + url.Values.Encode(), але це вже інша історія й інший контекст.

Схема: що відбувається, коли сервер читає query

Щоб закріпити картину, тримайте маленьку схему того, як мислить обробник. Тут немає магії — лише акуратні кроки, які легко повторювати щоразу.

flowchart TD
    A[HTTP-запит: /api/v1/tasks?tag=go&tag=http&done=true] --> B[net/http створює *http.Request]
    B --> C[r.URL: *url.URL]
    C --> D["r.URL.Query(): url.Values"]
    D --> E["Читаємо q.Get / q[...]"]
    E --> F[Далі: валідація та застосування до логіки]

На цій лекції ми впевнено доходимо до кроку «Читаємо q.Get / q[...]». Наступний крок — валідація та значення за замовчуванням — ми свідомо поки що не розбираємо, щоб не змішувати «як дістати» і «як правильно перевірити».

8. Типові помилки під час читання query-параметрів на сервері

Помилка № 1: парсити query вручну через strings.Split і «нібито працює».
Зазвичай це закінчується дивними помилками з екрануванням, плюсами замість пробілів, ключами, що повторюються, і питанням «чому в мене tag лише один». r.URL.Query() розв’язує це стандартно й надійно: ви отримуєте url.Values і працюєте з ним як із даними, а не як із рядком.

Помилка № 2: читати списки через Get, а потім дивуватися, що значень менше.
Get("tag") повертає лише перше значення. Якщо за контрактом у вас tag=go&tag=http, потрібно читати q["tag"]. Інакше ви ввічливо втрачаєте дані користувача, а потім робите вигляд, що так і задумано.

Помилка № 3: вважати, що Get("k") == "" означає «ключа немає».
Це неправильно: k= і відсутність k виглядають однаково для Get. Якщо вам важливо розрізняти ці випадки, перевіряйте наявність ключа через _, ok := q["k"]. Інакше значення за замовчуванням і помилки застосовуватимуться не туди, і користувачі почнуть знаходити у вас загадкові баги.

Помилка № 4: змішувати читання query та бізнес-логіку в одне полотно.
Коли в обробнику одночасно читаються параметри, парсяться числа, перевіряються діапазони, застосовуються фільтри й будується відповідь — код стає важким навіть для автора через тиждень. Набагато зрозуміліше спочатку дістати рядки з url.Values, а далі окремим кроком перетворювати їх на потрібні типи й перевіряти, повертаючи зрозумілі помилки.

Помилка № 5: намагатися «поправити» вхідний запит, змінюючи q як мапу.
На сервері запит — це вхідні дані. Якщо ви починаєте мутувати query, ви стираєте межу між «прийшло ззовні» і «ми самі собі вигадали». Це погіршує діагностику й може призвести до неочікуваних ефектів. Правильний підхід: зчитати параметри, а потім працювати вже зі своїми змінними або структурами.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ