JavaRush /Курси /Go SELF /net/url — розбір і складання URL без «склеювання рядків»

net/url — розбір і складання URL без «склеювання рядків»

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

1. Розбираємо URL через url.Parse

Коли новачок бачить URL, рука мимоволі тягнеться до старого доброго strings.Split. Це здається природним: «Тут є ?, отже можна відокремити query; тут є /, отже можна відокремити шлях». І певний час це навіть ніби працює. А потім у URL зʼявляється #fragment, далі — параметри з пробілами, потім %2F — закодований / усередині значення, і ваш «ідеальний» Split починає різати не те.

Саме тому в Go є пакет net/url: він привчає дивитися на URL як на структуровані дані, у яких є частини — схема, хост, шлях, query і fragment. І замість того щоб гадати, де там знак питання, ви читаєте поле u.RawQuery. Програмісти люблять передбачуваність — саме її й дає net/url.

Найчастіший сценарій: у нас є рядок, і ми хочемо отримати структуру *url.URL. Для цього є url.Parse, і вона повертає два значення: *url.URL і error. Помилку ігнорувати не можна: якщо URL хибний, краще дізнатися про це одразу, ніж потім налагоджувати, чому хост порожній.

Анатомія URL і поля url.URL

Будь-який URL у спрощеному вигляді можна подати так:

scheme://host/path?query#fragment

Суть у тому, що ці частини незалежні. Query не належить path, fragment не є продовженням query, а хост — це не «частина рядка до першого слеша», а окремий компонент. У Go це відображено структурою url.URL.

Зробімо невелику «карту» — що де лежить:

Частина URL Приклад Де це в Go (url.URL) Що варто памʼятати
Scheme
https
u.Scheme
Зазвичай
http
або
https
Host
api.example.com:443
u.Host
Може включати порт
Path
/api/v1/tasks
u.Path
Не містить
?query
Query
done=true&limit=20
u.RawQuery
Без символу
?
Fragment
top
u.Fragment
Без символу
#

Невеликий приклад із практики: URL https://golang.org/x/In%20Valid,X містить percent-encoding — тобто пробіл закодовано як %20. Якщо ви спробуєте вручну «різати» рядок і десь не в той момент розкодувати значення, дуже швидко створите собі проблеми.

Приклад: розбираємо повний URL


package main

import (
	"fmt"
	"net/url"
)

func main() {
	raw := "https://api.example.com/api/v1/tasks?done=true#top"

	u, err := url.Parse(raw)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	fmt.Println("scheme:", u.Scheme)     // scheme: https
	fmt.Println("host:", u.Host)         // host: api.example.com
	fmt.Println("path:", u.Path)         // path: /api/v1/tasks
	fmt.Println("rawQuery:", u.RawQuery) // rawQuery: done=true
	fmt.Println("fragment:", u.Fragment) // fragment: top
}

Зверніть увагу: RawQuery не містить ?, а Fragment не містить #. Це дуже зручно: вам не потрібно вручну відрізати службові символи — Go вже зробив це за вас.

Path, RawQuery і Fragment — три різні поля

Дуже типова плутанина: людина думає, що u.Path — це «все після домену». Але ні: «все після домену» — це вже суміш Path + ?query + #fragment. У Go ці речі рознесені по різних полях, і це справді зручно.

Уявіть, що ви пишете API, і у вас є шлях /api/v1/tasks, а query — це додаткові параметри запиту. Якщо ви випадково змішаєте їх, можна отримати баг рівня: «Чому роутер не знаходить обробник /api/v1/tasks?done=true?». Роутер дивиться на шлях, а query — не його частина.

Щоб відчути різницю на практиці, виведімо все в «лапках», щоб пробіли й порожні рядки були видимі:

package main

import (
	"fmt"
	"net/url"
)

func main() {
	u, err := url.Parse("https://api.example.com/api/v1/tasks?done=true#top")
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	fmt.Printf("Path=%q RawQuery=%q Fragment=%q\n",
		u.Path, u.RawQuery, u.Fragment,
	)
	// Path="/api/v1/tasks" RawQuery="done=true" Fragment="top"
}

І ось тут у багатьох стається маленький «клік»: URL — це не рядок, а структура.

3. Абсолютні та відносні URL

Наступна типова ситуація: ви розбираєте URL і раптом бачите, що u.Host == "". Здається, що Parse «зламався». Насправді це не помилка: ви, ймовірно, розпарсили відносний URL.

Відносний URL — це коли немає https://api.example.com, а є лише шлях, наприклад /api/v1/tasks?limit=20. Це цілком нормальна форма, особливо всередині застосунків, і Go її підтримує.

package main

import (
	"fmt"
	"net/url"
)

func main() {
	u, err := url.Parse("/api/v1/tasks?limit=20")
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	fmt.Printf("scheme=%q host=%q path=%q rawQuery=%q\n",
		u.Scheme, u.Host, u.Path, u.RawQuery,
	)
	// scheme="" host="" path="/api/v1/tasks" rawQuery="limit=20"
}

Чому це важливо? Тому що порожній хост не завжди означає помилку. Іноді це означає: «URL відносний, і так задумано».

4. Чому strings.Split за ? — це міна уповільненої дії

Зараз буде невелика історія з реального життя — без крові, але з легким запахом польових умов.

Якщо ви робите так: «знайду ?, усе після нього — query», ви забуваєте про те, що в URL трапляються закодовані символи (percent-encoding). Наприклад, пробіл може бути %20, а ще бувають ситуації, коли у значенні параметра є &, але він закодований як %26. Якщо ви «ріжете» рядок до декодування або після декодування не в тому порядку, можна отримати неправильний розбір.

Приклад із percent-encoding трапляється навіть у тестових URL на кшталт https://golang.org/x/In%20Valid,X. Там пробіл — частина шляху, але він поданий як %20. Ваш Split не зламається на %20, звісно, але зазвичай саморобний парсер обростає костилями: потім хтось додає «давайте декодувати весь рядок», і ось уже пробіл зʼявляється в шляху, а логіка порівняння та нормалізації починає чудити.

net/url розвʼязує цю проблему концептуально: спочатку він коректно розбирає структуру, а вже потім ви акуратно працюєте з частинами.

5. Збираємо URL через структуру url.URL

До цього ми говорили про те, як розібрати URL. Тепер важлива друга половина: як зібрати його так, щоб не склеювати вручну.

Коли ви склеюєте так:

base + "/api/v1/tasks" + "?done=true"

у вас зʼявляються питання:
що якщо base уже закінчується на /? Чи буде //?
що якщо параметрів немає? Чи не зʼявиться зайвий ??
що якщо значення містить пробіл? Як його екранувати?

Сьогодні ми ще не заглиблюємося в складання query-параметрів, але сам принцип складання URL через структуру ви маєте відчути вже зараз.

Приклад: зберемо URL для ресурсу tasks

package main

import (
	"fmt"
	"net/url"
)

func main() {
	u := url.URL{
		Scheme: "https",
		Host:   "api.example.com",
		Path:   "/api/v1/tasks",
	}

	fmt.Println(u.String()) // https://api.example.com/api/v1/tasks
}

Це виглядає майже надто просто. Але саме в цьому й сенс: ми не «малюємо рядок», а заповнюємо поля.

Базовий URL API як url.URL

Щоб приклади не були розрізненими, домовімося: у нашого навчального застосунку (трекера задач) є базова адреса API. Наприклад:

  • https://api.example.com (просто приклад домену),
  • і ресурс задач розміщено на /api/v1/tasks.

Дуже зручна практика — зберігати основу не рядком, а структурою url.URL. Тоді ви рідше помиляєтеся і простіше додаєте кінцеві шляхи.

package main

import (
	"fmt"
	"net/url"
)

func tasksListURL(base url.URL) url.URL {
	u := base          // копія структури (важливо: не вказівник)
	u.Path = "/api/v1/tasks"
	return u
}

func main() {
	base := url.URL{Scheme: "https", Host: "api.example.com"}

	u := tasksListURL(base)
	fmt.Println(u.String()) // https://api.example.com/api/v1/tasks
}

Тут є дуже приємний момент: url.URL — це структура, і присвоювання u := base робить копію. Тобто ви не псуєте базовий URL випадковими змінами.

6. Діагностика та модель ParseString

У реальній розробці ви найчастіше стикаєтеся не з «ідеальними» URL, а з ситуаціями на кшталт: «Чому в мене хост порожній?» або «Чому шлях раптом без слеша?». У такі моменти допомагає проста діагностика через fmt.Printf.

Відладочний принтер для URL

Зробімо маленький налагоджувальний принтер, який показує поля явно:

package main

import (
	"fmt"
	"net/url"
)

func debugURL(raw string) {
	u, err := url.Parse(raw)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	fmt.Printf("raw=%q\n", raw)
	fmt.Printf("scheme=%q host=%q path=%q rawQuery=%q fragment=%q\n",
		u.Scheme, u.Host, u.Path, u.RawQuery, u.Fragment,
	)
}

func main() {
	debugURL("https://api.example.com/api/v1/tasks?done=true#top")
}

Друк із %q — це ніби ввімкнути «режим рентгена»: ви бачите порожні рядки, спецсимволи, і все стає набагато зрозумілішим.

Мінісхема: що роблять Parse і String

Щоб закріпити логіку, корисно уявляти це не як магію, а як простий конвеєр: рядок перетворюється на структуру, а структура — назад у рядок.

flowchart TD
    A["Рядок URL
https://api.example.com/api/v1/tasks?done=true#top"] --> B["url.Parse(raw)"] B -->|ok| C["*url.URL
Scheme/Host/Path/RawQuery/Fragment"] B -->|err| D["error
Обробляємо й виходимо"] C --> E["u.String()"] E --> F["Зібраний рядок URL"]

Важлива ідея: ви перестаєте «жити» в рядках вручну і починаєте працювати з чітко розділеними частинами.

7. Типові помилки під час роботи з net/url

Помилка № 1: ігнорувати err від url.Parse.
Новачки інколи пишуть u, _ := url.Parse(raw) і думають: «Ну гаразд, воно ж майже завжди нормальне». А потім одного разу приходить хибний URL, u частково заповнений, і далі код працює зі сміттям. Якщо URL важливий для логіки — помилку парсингу треба обробити явно, навіть якщо поки що це лише fmt.Println і return.

Помилка № 2: плутати Path і RawQuery.
Дуже поширена помилка: порівнюють шлях з очікуваним, але беруть «усе після домену» разом із ?limit=20. У Go u.Path — це шлях без query, а u.RawQuery — це query без ?. Якщо тримати це в голові як «два різні відсіки», плутанини стає набагато менше.

Помилка № 3: вважати, що Fragment — частина query.
Фрагмент (#top) не бере участі в query-параметрах. Він існує окремо і, у браузерних сценаріях, зазвичай не надсилається на сервер так, як ви могли б очікувати. Навіть якщо ви зараз не пишете браузер, корисно просто памʼятати: u.Fragment — окреме поле, не змішуйте його з u.RawQuery.

Помилка № 4: «склеїти» URL через + і забути про початковий / у шляху.
Одна з найприкріших дрібниць: ви написали Path: "api/v1/tasks" замість "/api/v1/tasks", отримали дивний URL і потім налагоджуєте, чому сервер відповідає 404. Під час складання через url.URL дисципліна простіша: ви бачите Path як окреме поле і швидше помічаєте, що слеш загубився.

Помилка № 5: лякатися порожнього Host після Parse.
Якщо ви розпарсили /api/v1/tasks?limit=20, то Scheme і Host будуть порожніми, і це нормально: ви розібрали відносний URL. Помилка тут радше психологічна: здається, що «дані зникли», хоча насправді це просто інша форма URL.

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