JavaRush /Курси /Go SELF /Path‑параметри {id} та витягування через r.PathValue("id"...

Path‑параметри {id} та витягування через r.PathValue("id")

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

1. Чому id у шляху — це мова API

Коли ви вперше бачите URL на кшталт /tasks/42, виникає спокуса подумати: «Ну й навіщо так складно? Не можна було написати /tasks?id=42?» Можна. Але в API є своя «мова»: шлях у URL зазвичай описує ресурс, а query‑параметри — це параметри запиту. Це схоже на адресу доставки: вулиця й будинок — це «куди», а коментар курʼєру — «як саме».

Якщо в нас є список задач, то /api/v1/tasks читається як «колекція задач». А /api/v1/tasks/42 — як «конкретна задача з ідентифікатором 42». Це допомагає підтримувати зрозумілу структуру API: шлях відповідає на запитання «що саме ми чіпаємо», а не перетворюється на мішанину параметрів.

Головна практична причина, без зайвої філософії, така: цей стиль простіше читати, документувати й розширювати. А ще він чудово лягає на роутинг: ми можемо сказати роутеру «ось тут стоїть {id}, підстав сюди один довільний сегмент шляху — і передай його обробнику».

2. Path‑параметр {id} у ServeMux: як він зіставляється

Коли ми говоримо «path‑параметр», ми маємо на увазі змінну частину шляху, яка займає рівно один сегмент між слешами. Це важливо: {id} — не «будь-який рядок будь-якої довжини», а саме «те, що стоїть між / і наступним /». Тому /tasks/{id} зіставляється з /tasks/42, але не перетворюється на пилосос, який засмоктує пів інтернету.

Починаючи з Go 1.22, шаблони net/http.ServeMux підтримують методи, wildcard’и та захоплення сегментів виду {id}; а значення захопленого сегмента можна дістати через r.PathValue. Це означає, що нам не потрібно вручну розбирати r.URL.Path, робити strings.Split, ловити помилку на одиницю й пояснювати собі, чому в продакшені раптом прилетів шлях із подвійним слешем.

Мінімальна реєстрація маршруту виглядає так:

package main

import (
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /api/v1/tasks/{id}", handleGetTask)

	_ = http.ListenAndServe(":8080", mux)
}

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	// Поки порожньо — у наступних розділах навчимося діставати id.
}

Зверніть увагу на дві речі. По-перше, {id} — це ім’я параметра. По-друге, ім’я параметра — частина контракту: якщо ви написали {id}, то витягувати треба саме "id", а не "taskId" і не "ID".

3. Витягування та імена параметрів

Витягування значення: r.PathValue("id")

Тепер до найцікавішого: як дістати те, що реально прийшло в URL.

r.PathValue("id") повертає рядок — тобто «сире» значення сегмента шляху. На цьому етапі воно ще не число, не UUID і не «точно коректне значення». Це просто текст із URL. І це добре: витягування й перевірка — різні задачі. Сьогодні ми чесно робимо лише витягування, а перевірку й перетворення на число централізуємо окремо, щоб не розмножувати копіпасту.

Ось мінімальний приклад обробника, який показує, що саме прилетіло:

package main

import (
	"fmt"
	"net/http"
)

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	fmt.Fprintf(w, "task id = %q\n", id) // task id = "42"
}

Тут %q корисний тим, що він друкує рядок у лапках. Якщо раптом id порожній, ви це побачите як "", а не будете гадати, «воно надрукувалося чи ні».

Що поверне PathValue, якщо параметра немає

Зазвичай у правильно зареєстрованому маршруті параметр буде. Але якщо ви помилилися в імені (наприклад, у патерні {taskID}, а читаєте "id"), PathValue поверне порожній рядок. У цьому місці багато новачків починають «лікувати симптоми» й ставити зайві перевірки, хоча правильний шлях — привести до ладу імена параметрів, щоб вони збігалися.

Регістр важливий: ім’я в {...} і ключ у PathValue(...) мають збігтися

У програмуванні половина багів — це «ми домовилися, але забули, про що». У path‑параметрах це проявляється особливо яскраво: ім’я параметра в {...} і рядок у PathValue(...) мають збігатися символ у символ, включно з регістром.

Уявіть, що {id} — це наклейка на коробці. Якщо потім ви шукаєте коробку за наклейкою "ID", а наклейка була "id", то формально ви «шукаєте інше». Go вам нічого не винен — він просто поверне порожній рядок.

Приклад правильного збігу:

mux.HandleFunc("GET /api/v1/tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	_, _ = w.Write([]byte(id + "\n"))
})

А ось приклад дуже життєвої помилки:

mux.HandleFunc("GET /api/v1/tasks/{taskID}", func(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id") // помилка: ім’я не збігається, id буде ""
	_, _ = w.Write([]byte(id + "\n"))
})

Як краще називати: {id} чи {taskID}

Якщо в маршруті лише один ідентифікатор і з контексту зрозуміло, чий він, {id} — нормально. Якщо параметрів кілька, наприклад користувач і задача, тоді краще писати конкретніше: {userID} і {taskID}. Це підвищує читабельність коду без жодних коментарів.

4. Кілька параметрів і query‑параметри

Кілька параметрів у шляху: приклад /users/{userID}/tasks/{taskID}

Коли API починає жити — тобто коли ви перестаєте писати «Hello world» і починаєте писати «схоже, це справжній сервер» — дуже швидко з’являються вкладені ресурси: задачі користувача, коментарі до задачі, вкладення до коментаря тощо. Саме тут path‑параметри починають справді економити час і нерви.

Подивімося на приклад маршруту з двома параметрами:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /api/v1/users/{userID}/tasks/{taskID}", func(w http.ResponseWriter, r *http.Request) {
		userID := r.PathValue("userID")
		taskID := r.PathValue("taskID")
		fmt.Fprintf(w, "user=%s task=%s\n", userID, taskID) // user=7 task=42
	})

	_ = http.ListenAndServe(":8080", mux)
}

Зверніть увагу: ми не розбираємо r.URL.Path вручну. Ми не рахуємо слеші. Ми не сперечаємося із собою о третій годині ночі, чому індекс 3, а не 2. Ми просто просимо запит: «дай мені "userID"» і «дай мені "taskID"».

Якщо ви колись вручну розбирали шляхи через strings.Split, зараз маєте відчути легку радість. Якщо ні — нічого, відчуєте її після першого бага в продакшені.

Path vs query‑параметри: у чому різниця

Дуже часта плутанина в новачків: «А {id} — це те саме, що ?id=...?» Ні. Вони схожі тим, що обидва дають вам рядок, але сенс у них зазвичай різний.

Порівняймо на практиці:

Що хочемо виразити Приклад URL Де лежить значення в Go
«конкретний ресурс»
/api/v1/tasks/42
r.PathValue("id")
«параметри запиту»
/api/v1/tasks?limit=10
r.URL.Query().Get("limit")
«фільтр/режим виводу»
/api/v1/tasks?done=true
r.URL.Query().Get("done")

Невеликий приклад: нехай у нас є кінцева точка задачі за id, і ми додали «режим багатослівності» через query‑параметр verbose — суто для демонстрації відмінностей:

package main

import (
	"fmt"
	"net/http"
)

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	verbose := r.URL.Query().Get("verbose")

	fmt.Fprintf(w, "id=%s verbose=%s\n", id, verbose) // id=42 verbose=1
}

У цьому прикладі id — частина шляху, бо це «яку задачу ми читаємо», а verbose — query, бо це «як саме показати відповідь».

5. Вбудовуємо в навчальний застосунок

Зараз ми зробимо маленький, але важливий крок: додамо обробник, який витягує {id} і повертає JSON‑відповідь. Ми поки не будемо перетворювати id на int і не будемо доводити, що він «точно правильний» — наша ціль сьогодні навчитися акуратно діставати значення без ручного парсингу шляху.

Припустімо, що writeJSON у нас уже є з минулих лекцій (він ставить Content-Type, пише статус і кодує структуру в JSON). Тоді обробник може виглядати так:

package main

import (
	"net/http"
)

type taskResponse struct {
	ID string `json:"id"`
}

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	writeJSON(w, http.StatusOK, taskResponse{ID: id})
}

Якщо дуже хочеться «помацати» очима без JSON‑хелпера, можна тимчасово зробити так:

package main

import (
	"fmt"
	"net/http"
)

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	fmt.Fprintf(w, `{"id":%q}`+"\n", id) // {"id":"42"}
}

Цей варіант не ідеальний: ручна збірка JSON — це як лагодити годинник молотком. Іноді працює, але краще не звикати. Зате він добре показує, що id — рядок і ми справді його отримали.

Маленька пересторога: перевірка на порожній рядок

Іноді не зайвою буде невелика допоміжна функція, яка робить порожній рядок явною умовою. Одразу важливе застереження: якщо маршрут зареєстровано правильно й ім’я параметра збігається, id порожнім бути не повинен. Тому така перевірка — не «валідація користувача», а радше захист від наших друкарських помилок і рефакторингу.

Наприклад:

package main

import "net/http"

func mustPathValue(r *http.Request, key string) (string, bool) {
	v := r.PathValue(key)
	return v, v != ""
}

Використання:

package main

import "net/http"

func handleGetTask(w http.ResponseWriter, r *http.Request) {
	id, ok := mustPathValue(r, "id")
	if !ok {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	_ = id // далі використовуємо id
	w.WriteHeader(http.StatusOK)
}

Чому тут 500, а не 400? Тому що якщо id не знайдено, найчастіше винні не ви, а ми: маршрут і код роз’їхалися в іменах. Користувач міг надіслати будь-який URL, але якщо обробник уже викликано, значить, ServeMux вирішив, що маршрут збігся. А якщо збігся, але параметр не витягується, це дуже схоже на помилку в коді.

6. Типові помилки під час роботи з {id} і r.PathValue(...)

Помилка №1: ім’я в {...} не збігається з ключем у PathValue(...).
Найчастіший баг — друкарська помилка або різний стиль іменування: {taskID} у маршруті та r.PathValue("taskId") у коді. У результаті PathValue повертає порожній рядок, а ви потім годину шукаєте, чому id зник. Лікується дисципліною: один стиль і уважність до регістру.

Помилка №2: спроба дістати path‑параметр через r.URL.Query().
Query() працює тільки з частиною після ?. Якщо ви пишете /tasks/42, то 42 — не query, а сегмент шляху. Тому r.URL.Query().Get("id") поверне порожнечу, і це нормально. Використовуйте r.PathValue("id") для {id} і Query() для ?x=y.

Помилка №3: ручний парсинг r.URL.Path замість PathValue.
Можна зробити strings.Split(r.URL.Path, "/"), потім узяти «третій елемент» і сподіватися на краще. Але це крихко: зайвий слеш, інший префікс, версія API — і все ламається. Вбудований механізм {id} + PathValue якраз придуманий, щоб не грати в «вгадай індекс».

Помилка №4: змішування витягування й перетворення «в один рядок».
Іноді пишуть: strconv.Atoi(r.PathValue("id")) і радіють, доки не прилетить "abc" і не перетвориться на «0, але наче працює». На рівні звички краще розділяти кроки: спочатку дістали рядок, потім окремо — і централізовано — розпарсили та перевірили.

Помилка №5: використання одного й того самого {id} для різних сутностей в одному шляху.
Маршрут /users/{id}/tasks/{id} технічно виглядає «логічно», але в коді перетворюється на кашу: який id ви зараз читаєте? Краще явно: {userID} і {taskID}. Це той випадок, коли зайві дві літери економлять вам десятки хвилин налагодження.

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