Aplikasi nyata memiliki puluhan endpoint: daftar item, detail item, form, aset statis, halaman utama. Sebelum Go 1.22, http.ServeMux hanya mencocokkan path sehingga banyak proyek memasang router pihak ketiga untuk membedakan method dan menangkap parameter. Sejak Go 1.22, ServeMux mengenal pola method + path dan wildcard {id}, sehingga kebutuhan routing umum terpenuhi tanpa dependency. Chapter ini menyusun pola, membaca parameter dengan r.PathValue, dan membuktikan perilaku 405 serta 404 lewat httptest.
Pola Method dan Path #
Pola ServeMux modern ditulis sebagai "METHOD PATH". Contoh berikut mendaftarkan tiga rute untuk resource item:
api.HandleFunc("GET /items", ambilItems)
api.HandleFunc("POST /items", simpanItem)
api.HandleFunc("GET /items/{id}", detailItem)Pola "GET /items" hanya cocok untuk request GET dengan path persis /items. Segmen {id} adalah wildcard yang menangkap satu segmen path apa pun, lalu nilainya dibaca di dalam handler:
func detailItem(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "detail item: %s\n", r.PathValue("id"))
}Akar Persis, Subtree, dan Tangkapan Umum #
ServeMux mengenali tiga bentuk pola path yang saling melengkapi. Tabel berikut merangkum ketiganya:
| Pola | Cocok untuk | Sifat |
|---|---|---|
"GET /items" | hanya path /items | persis |
"GET /items/{id}" | /items/7, /items/abc | wildcard satu segmen |
"GET /static/" | /static/ beserta seluruh isinya | subtree |
"GET /{$}" | hanya path / | akar persis |
"/" | semua path yang tidak cocok pola lain | tangkapan umum |
Pola berakhiran garis miring seperti "/static/" dan "/" adalah subtree: pola tersebut menangkap seluruh path di bawahnya. Akibatnya "/" akan menelan seluruh request yang tidak cocok pola lain. Pola khusus "/{$}" hadir untuk membedakan: penanda {$} hanya cocok pada akar persis, sehingga request GET / bisa mendapat handler sendiri tanpa mengorbankan tangkapan umum. Ketika beberapa pola cocok untuk satu request, ServeMux memilih pola yang paling spesifik — aturan precedence inilah yang membuat "/static/app.css" jatuh ke subtree "GET /static/", bukan ke "/".
Respons 405 dan 404 dari ServeMux #
ServeMux menjawab dua situasi gagal secara otomatis. Pertama, apabila path cocok tetapi method tidak, ServeMux membalas status 405 Method Not Allowed lengkap dengan header Allow berisi daftar method yang tersedia — contoh program memperlihatkan Allow: GET, HEAD untuk request DELETE /items/7 karena hanya pola GET yang terdaftar untuk path tersebut; method HEAD otomatis diizinkan mengikuti GET. Kedua, apabila tidak ada pola yang cocok sama sekali, ServeMux membalas 404 dengan body bawaan 404 page not found.
Pola Invalid Memicu Panic #
Validasi pola terjadi pada saat registrasi, bukan saat request masuk. Pola seperti "GET" — method tanpa path — langsung memicu panic:
panic: parsing "GET": at offset 0: host/path missing /Perilaku fail fast ini disengaja: kesalahan pola sebaiknya menghentikan program saat pengembangan, bukan ditemukan diam-diam di produksi. Pola lain yang ditolak antara lain wildcard tanpa penutup } dan nama wildcard kembar dalam satu pola.
ServeMux atau Router Pihak Ketiga #
Router pihak ketiga seperti chi dan gorilla/mux tetap populer karena menawarkan fitur tambahan — pengelompokan rute dan middleware terpasang di router. Untuk kebutuhan routing standar, ServeMux kini memadai dan bebas dependency; keputusan memasang router tambahan sebaiknya ditunda sampai ServeMux benar-benar tidak memenuhi kebutuhan Anda.
Program Utuh: Dua Mux dalam Satu Berkas #
Berkas routing-demo.go pada bagian bawah halaman membangun dua mux: mux api dengan pola method dan path, serta mux situs dengan akar persis, subtree, dan tangkapan umum, lalu menutup dengan pembuktian panic pola invalid.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
// Mux pertama: pola method + path untuk API item (tanpa tangkapan umum).
api := http.NewServeMux()
api.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "daftar item")
})
api.HandleFunc("POST /items", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "item dibuat")
})
api.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "detail item: %s\n", r.PathValue("id"))
})
fmt.Println("== mux api (pola method + path)")
uji(api, "GET", "/items")
uji(api, "GET", "/items/7")
uji(api, "POST", "/items")
uji(api, "DELETE", "/items/7")
uji(api, "GET", "/takada")
// Mux kedua: akar persis, subtree, dan tangkapan umum.
situs := http.NewServeMux()
situs.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "halaman utama")
})
situs.HandleFunc("GET /static/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "aset statis: %s\n", r.URL.Path)
})
situs.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "halaman tidak ditemukan", http.StatusNotFound)
})
fmt.Println("== mux situs (akar persis, subtree, tangkapan umum)")
uji(situs, "GET", "/")
uji(situs, "GET", "/static/app.css")
uji(situs, "GET", "/profil")
// Pola yang tidak valid memicu panic pada saat registrasi.
fmt.Println("registrasi pola \"GET\" (tanpa path):")
func() {
defer func() { fmt.Println(" panic:", recover()) }()
m := http.NewServeMux()
m.HandleFunc("GET", func(w http.ResponseWriter, r *http.Request) {})
}()
}
func uji(h http.Handler, method, path string) {
req := httptest.NewRequest(method, path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("%-7s %-15s -> %d | %s\n", method, path, rec.Code,
strings.TrimSpace(rec.Body.String()))
if rec.Code == http.StatusMethodNotAllowed {
fmt.Printf(" Allow: %s\n", rec.Header().Get("Allow"))
}
}Jalankan berkas tersebut dengan perintah berikut dari direktori chapter:
go run routing-demo.goOutput yang Dihasilkan:
== mux api (pola method + path)
GET /items -> 200 | daftar item
GET /items/7 -> 200 | detail item: 7
POST /items -> 201 | item dibuat
DELETE /items/7 -> 405 | Method Not Allowed
Allow: GET, HEAD
GET /takada -> 404 | 404 page not found
== mux situs (akar persis, subtree, tangkapan umum)
GET / -> 200 | halaman utama
GET /static/app.css -> 200 | aset statis: /static/app.css
GET /profil -> 404 | halaman tidak ditemukan
registrasi pola "GET" (tanpa path):
panic: parsing "GET": at offset 0: host/path missing /Mari kita telaah baris demi baris program di atas:
- Baris 13-22 (
api.HandleFunc("GET /items", ...)dst.): mendaftarkan tiga pola method + path; wildcard{id}menangkap segmen terakhir dan nilainya dibaca lewatr.PathValue. - Baris 25-29 (
uji(api, ...)): mengujiGET,POST, danDELETEterhadap muxapi; requestDELETE /items/7berakhir405karena hanya polaGETyang cocok untuk path tersebut. - Baris 29 (
uji(api, "GET", "/takada")): membuktikan 404 bawaan404 page not foundketika tidak ada satu pun pola yang cocok. - Baris 33-41 (
situs.HandleFunc("GET /{$}", ...)dst.): menyusun akar persis"/{$}", subtree"/static/", dan tangkapan umum"/"pada mux kedua. - Baris 44-46 (
uji(situs, ...)): membuktikan precedence —GET /jatuh ke akar persis,GET /static/app.cssjatuh ke subtree, danGET /profiljatuh ke tangkapan umum yang membalas 404. - Baris 50-54 (
func() { defer ... }): mendaftarkan pola invalid"GET"di dalam fungsi dengandefer recoveragar panic tertangkap dan pesannya tercetak tanpa menghentikan program. - Baris 57-65 (
func uji(...)): helper yang menjalankan satu request tiruan, mencetak status dan body, lalu menambahkan headerAllowsetiap kali statusnya 405.
Routing kini berpola rapi, tinggal memastikan handler membaca input dengan benar. Chapter berikutnya membedah pembacaan query, header, dan body pada sisi request serta penulisan response JSON yang rapi.
Referensi #
- Dokumentasi http.ServeMux — aturan pola, precedence, dan wildcard.
- Dokumentasi http.NewServeMux — konstruktor mux baru.
- Dokumentasi package httptest — alat uji request dan perekam response.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
routing-demo.go
Unduh berkaspackage main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
// Mux pertama: pola method + path untuk API item (tanpa tangkapan umum).
api := http.NewServeMux()
api.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "daftar item")
})
api.HandleFunc("POST /items", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "item dibuat")
})
api.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "detail item: %s\n", r.PathValue("id"))
})
fmt.Println("== mux api (pola method + path)")
uji(api, "GET", "/items")
uji(api, "GET", "/items/7")
uji(api, "POST", "/items")
uji(api, "DELETE", "/items/7")
uji(api, "GET", "/takada")
// Mux kedua: akar persis, subtree, dan tangkapan umum.
situs := http.NewServeMux()
situs.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "halaman utama")
})
situs.HandleFunc("GET /static/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "aset statis: %s\n", r.URL.Path)
})
situs.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "halaman tidak ditemukan", http.StatusNotFound)
})
fmt.Println("== mux situs (akar persis, subtree, tangkapan umum)")
uji(situs, "GET", "/")
uji(situs, "GET", "/static/app.css")
uji(situs, "GET", "/profil")
// Pola yang tidak valid memicu panic pada saat registrasi.
fmt.Println("registrasi pola \"GET\" (tanpa path):")
func() {
defer func() { fmt.Println(" panic:", recover()) }()
m := http.NewServeMux()
m.HandleFunc("GET", func(w http.ResponseWriter, r *http.Request) {})
}()
}
func uji(h http.Handler, method, path string) {
req := httptest.NewRequest(method, path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("%-7s %-15s -> %d | %s\n", method, path, rec.Code,
strings.TrimSpace(rec.Body.String()))
if rec.Code == http.StatusMethodNotAllowed {
fmt.Printf(" Allow: %s\n", rec.Header().Get("Allow"))
}
}