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:

KomponenNilaiProperti di Go
Schemehttpsu.Scheme
Userpengguna:rahasiau.User
Hostapi.kontenku.id:443u.Host
Path/artikel/2026u.Path
Querykategori=golang&halaman=2u.RawQuery
Fragmentkomentaru.Fragment
Hostnameapi.kontenku.idu.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.

Referensi

  1. 1Package net/url — Go Standard Library Documentation
  2. 2URL Parsing — Go by Example