BAB 46: URL Parsing dengan net/url
Pelajari cara mengurai URL menjadi komponen terstruktur menggunakan package net/url — dari skema dan host hingga query string yang kompleks.
Server catatan yang dibangun di bab sebelumnya membaca query string dengan r.URL.Query().Get("nama") — satu baris yang terlihat sederhana. Tapi di balik satu baris itu, ada struktur data bernama url.URL yang sudah memegang seluruh informasi URL secara terurai: skema, host, path, query, dan fragment. Saat handler dipanggil, Go sudah melakukan parsing tersebut sebelum kode handler dijalankan.
Tidak semua kebutuhan URL parsing datang dari dalam handler. Kadang program perlu mem-parsing URL yang datang dari file konfigurasi, database, atau argumen command-line — dan untuk itu, package net/url adalah alat yang tepat.
Anatomi Sebuah URL
Sebelum masuk ke kode, penting untuk memahami komponen-komponen yang membentuk sebuah URL. Ambil contoh URL berikut:
https://pengguna:rahasia@api.kontenku.id:443/artikel/2026?kategori=golang&halaman=2#komentar
URL ini punya tujuh komponen berbeda:
| Komponen | Nilai | Properti di Go |
|---|---|---|
| Scheme | https | u.Scheme |
| User | pengguna:rahasia | u.User |
| Host | api.kontenku.id:443 | u.Host |
| Path | /artikel/2026 | u.Path |
| Query | kategori=golang&halaman=2 | u.RawQuery |
| Fragment | komentar | u.Fragment |
| Hostname | api.kontenku.id | u.Hostname() |
u.Host menyimpan host beserta port, sedangkan u.Hostname() memisahkan keduanya dan hanya mengembalikan nama host. Ini perbedaan kecil tapi sering menjadi sumber kebingungan.
Mem-parsing URL dengan url.Parse
Fungsi url.Parse() mengubah string URL menjadi struct *url.URL yang bisa diakses field per field.
// url-info.go
package main
import (
"fmt"
"net/url"
)
func main() {
rawURL := "https://pengguna:rahasia@api.kontenku.id:443/artikel/2026?kategori=golang&halaman=2#komentar"
hasil, err := url.Parse(rawURL)
if err != nil {
fmt.Println("gagal parse URL:", err)
return
}
fmt.Println("scheme :", hasil.Scheme)
fmt.Println("host :", hasil.Host)
fmt.Println("hostname :", hasil.Hostname())
fmt.Println("port :", hasil.Port())
fmt.Println("path :", hasil.Path)
fmt.Println("query :", hasil.RawQuery)
fmt.Println("fragment :", hasil.Fragment)
}
Output:
scheme : https
host : api.kontenku.id:443
hostname : api.kontenku.id
port : 443
path : /artikel/2026
query : kategori=golang&halaman=2
fragment : komentar
url.Parse() mengembalikan error, dan penanganannya tidak boleh diabaikan. URL yang tampak valid secara visual bisa saja gagal di-parse karena karakter yang tidak valid atau encoding yang salah.
Membaca Query String
hasil.RawQuery menyimpan query string dalam bentuk mentah: kategori=golang&halaman=2. Untuk membaca nilai per parameter, gunakan hasil.Query() yang mengembalikan url.Values — sebuah map[string][]string.
// url-query.go
package main
import (
"fmt"
"net/url"
)
func main() {
rawURL := "https://api.kontenku.id/artikel?kategori=golang&kategori=tutorial&halaman=2&terbit=true"
hasil, err := url.Parse(rawURL)
if err != nil {
fmt.Println("gagal parse URL:", err)
return
}
params := hasil.Query()
fmt.Println("semua kategori :", params["kategori"])
fmt.Println("kategori pertama:", params.Get("kategori"))
fmt.Println("halaman :", params.Get("halaman"))
fmt.Println("terbit :", params.Get("terbit"))
fmt.Println("ada 'tag'? :", params.Has("tag"))
}
Output:
semua kategori : [golang tutorial]
kategori pertama: golang
halaman : 2
terbit : true
ada 'tag'? : false
Satu key bisa punya beberapa nilai — inilah kenapa url.Values menyimpan []string per key, bukan string. params.Get() selalu mengembalikan nilai pertama, sedangkan akses langsung params["kategori"] mengembalikan seluruh slice.
params.Has("key") berguna untuk membedakan parameter yang ada tapi kosong (?tag=) dengan parameter yang memang tidak ada sama sekali. Keduanya akan mengembalikan string kosong dengan params.Get("tag"), tapi Has() bisa membedakannya.
Membangun URL Secara Programatik
Selain parsing, net/url juga bisa digunakan untuk membangun URL dari komponen-komponennya. Ini lebih aman daripada string concatenation karena encoding karakter khusus ditangani otomatis.
// url-builder.go
package main
import (
"fmt"
"net/url"
)
func main() {
params := url.Values{}
params.Set("judul", "belajar go & net/url")
params.Set("halaman", "1")
params.Add("tag", "golang")
params.Add("tag", "tutorial")
endpoint := url.URL{
Scheme: "https",
Host: "api.kontenku.id",
Path: "/cari",
RawQuery: params.Encode(),
}
fmt.Println(endpoint.String())
}
Output:
https://api.kontenku.id/cari?halaman=1&judul=belajar+go+%26+net%2Furl&tag=golang&tag=tutorial
Perhatikan "belajar go & net/url" berubah menjadi belajar+go+%26+net%2Furl — spasi di-encode sebagai +, dan karakter & serta / di-escape agar tidak merusak struktur query string. params.Encode() melakukan semua ini secara otomatis dan mengurutkan key secara alfabetis.
Jangan pernah membangun query string dengan string concatenation seperti "?judul=" + judul. Jika judul mengandung & atau =, query string akan rusak dan bisa menjadi celah keamanan. Selalu gunakan url.Values.Encode().
Membaca Informasi Pengguna dari URL
Beberapa URL menyertakan kredensial dalam formatnya — seperti connection string database atau URL autentikasi. u.User menyimpan informasi ini sebagai *url.Userinfo.
// url-userinfo.go
package main
import (
"fmt"
"net/url"
)
func main() {
rawURL := "postgres://dbadmin:p@sswOrd123@localhost:5432/kontenku_db?sslmode=disable"
hasil, err := url.Parse(rawURL)
if err != nil {
fmt.Println("gagal parse URL:", err)
return
}
if hasil.User != nil {
namaUser := hasil.User.Username()
password, adaPassword := hasil.User.Password()
fmt.Println("username :", namaUser)
fmt.Println("ada password:", adaPassword)
if adaPassword {
fmt.Println("password :", password)
}
}
fmt.Println("host :", hasil.Host)
fmt.Println("path (db) :", hasil.Path)
fmt.Println("query :", hasil.RawQuery)
fmt.Println("url aman :", hasil.Redacted())
}
Output:
username : dbadmin
ada password: true
password : p@sswOrd123
host : localhost:5432
path (db) : /kontenku_db
query : sslmode=disable
url aman : postgres://dbadmin:xxxxx@localhost:5432/kontenku_db?sslmode=disable
hasil.Redacted() menghasilkan string URL yang sama persis dengan aslinya, kecuali password diganti dengan xxxxx. Sangat berguna untuk logging: URL bisa dilog untuk debugging tanpa risiko kredensial terekspos di log file.
Studi Kasus: Validator dan Parser Konfigurasi URL
Menggabungkan semua yang sudah dipelajari untuk membangun fungsi yang memvalidasi dan mengekstrak informasi dari URL konfigurasi layanan.
// url-config.go
package main
import (
"fmt"
"net/url"
"strconv"
)
type KonfigurasiLayanan struct {
Protokol string
Host string
Port int
BasePath string
Timeout int
Debug bool
}
func parseKonfigurasi(rawURL string) (*KonfigurasiLayanan, error) {
hasil, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("URL tidak valid: %w", err)
}
if hasil.Scheme == "" || hasil.Host == "" {
return nil, fmt.Errorf("URL harus lengkap dengan skema dan host")
}
portStr := hasil.Port()
port := 80
if portStr != "" {
port, err = strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("port tidak valid: %w", err)
}
}
params := hasil.Query()
timeout := 30
if tStr := params.Get("timeout"); tStr != "" {
timeout, err = strconv.Atoi(tStr)
if err != nil {
return nil, fmt.Errorf("nilai timeout tidak valid")
}
}
debug := params.Get("debug") == "true"
return &KonfigurasiLayanan{
Protokol: hasil.Scheme,
Host: hasil.Hostname(),
Port: port,
BasePath: hasil.Path,
Timeout: timeout,
Debug: debug,
}, nil
}
func main() {
urlKonfigurasi := "https://layanan.kontenku.id:8443/api/v2?timeout=60&debug=true"
config, err := parseKonfigurasi(urlKonfigurasi)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Protokol : %s\n", config.Protokol)
fmt.Printf("Host : %s\n", config.Host)
fmt.Printf("Port : %d\n", config.Port)
fmt.Printf("Base Path: %s\n", config.BasePath)
fmt.Printf("Timeout : %d detik\n", config.Timeout)
fmt.Printf("Debug : %v\n", config.Debug)
}
Output:
Protokol : https
Host : layanan.kontenku.id
Port : 8443
Base Path: /api/v2
Timeout : 60 detik
Debug : true
Fungsi parseKonfigurasi melakukan validasi awal sebelum mengekstrak nilai, dan menggunakan strconv.Atoi() untuk mengkonversi parameter string menjadi integer — pola yang konsisten dengan teknik konversi tipe yang sudah dipelajari di bab sebelumnya.
Latihan
Latihan 1 — Normalisasi URL:
Tulis fungsi normalisasiURL(raw string) (string, error) yang menerima URL apapun dan mengembalikan versi yang sudah dinormalisasi: skema selalu huruf kecil, fragment dihapus, dan query string diurutkan ulang secara alfabetis. Hint: parse dulu, modifikasi field yang diperlukan, lalu panggil .String().
Latihan 2 — Pembanding domain:
Tulis fungsi samaDomain(url1, url2 string) bool yang mengembalikan true jika dua URL berada di domain yang sama (abaikan port dan path). Tambahkan penanganan kasus edge: salah satu URL tidak valid, atau salah satu tidak punya host.
Latihan 3 — Query string merger:
Tulis fungsi gabungQuery(base, tambahan string) (string, error) yang menerima dua URL string dan mengembalikan URL pertama dengan query string dari kedua URL digabung. Jika ada key yang sama, nilai dari URL kedua menimpa nilai dari URL pertama.
Dengan net/url, URL bukan lagi sekadar string yang harus di-split manual. Setiap komponen bisa diakses, dimodifikasi, dan dibangun ulang dengan cara yang aman secara encoding. Fondasi ini menjadi semakin penting ketika program Go mulai berkomunikasi dengan API eksternal — dan itu adalah arah yang akan ditempuh di bab-bab selanjutnya, ketika program menjadi bukan hanya server yang menerima request, tapi juga klien yang mengirimnya.