- Peningkatan ServeMux
net/http di Go 1.22 menambahkan pencocokan pola yang lebih kuat ke multiplexer HTTP standar, sehingga mengurangi area yang sebelumnya bergantung pada router pihak ketiga karena pencocokan path yang terlalu sederhana
- Mux baru memungkinkan penggunaan metode HTTP dan wildcard path seperti
/task/{id}/ di dalam pola yang sama, sehingga path yang sama bisa dihubungkan ke handler berbeda untuk GET, POST, dan DELETE
- Nilai yang cocok bisa dibaca dengan
req.PathValue("id"), dan melalui {id}... serta {$} kita bisa mengontrol seluruh path lanjutan atau akhir path yang persis
- Konflik pola akan muncul sebagai panic saat pendaftaran, sekaligus memberi tahu path mana yang cocok di kedua sisi dan mengapa tidak ada salah satu yang lebih spesifik
- Meskipun semakin banyak server yang bisa cukup menggunakan mux standar saja, router atau framework pihak ketiga seperti
gorilla/mux dan Gin tetap menyediakan fitur dan alat yang lebih luas
Perubahan ServeMux di Go 1.22
- Go 1.22 akan menghadirkan pencocokan pola yang diperkuat ke
http.ServeMux, multiplexer penyajian HTTP bawaan dari net/http
http.ServeMux sebelumnya berfokus pada pencocokan path dasar, sehingga library pihak ketiga banyak dipakai untuk routing yang lebih kompleks
- Mux baru membawa pencocokan lanjutan ke dalam standard library dan memperkecil kesenjangan fitur dengan paket pihak ketiga
- Sebagai pembanding, digunakan contoh server REST dengan pendekatan standard library lama dan dengan
gorilla/mux
Cara dasar menggunakan mux baru
- Bagi pengembang yang pernah memakai paket router Go seperti
gorilla/mux, cara penggunaan mux standar baru ini akan terasa familiar
- Contohnya membuat mux dengan
http.NewServeMux() lalu mendaftarkan pola yang lebih kaya ke HandleFunc
mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "got path\n")
})
mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "handling task with id=%v\n", id)
})
- Handler pertama menyertakan metode
GET di dalam pola, sehingga hanya berjalan untuk permintaan GET yang dimulai dengan /path/
- Handler kedua mencocokkan satu komponen path melalui
{id} pada /task/{id}/, lalu membaca nilainya di handler dengan r.PathValue("id")
- Karena contoh ini dibuat saat Go 1.22 belum dirilis, menjalankannya dengan
gotip disarankan
- Hasil pengujian langsung menunjukkan aturan routing
- Permintaan
/what/ menghasilkan 404 page not found
- Permintaan
GET ke /path/ menghasilkan got path
- Permintaan
POST ke /path/ menghasilkan Method Not Allowed
- Permintaan
/task/f0cd2e/ diproses dengan id=f0cd2e
Fitur pola tambahan dan penanganan konflik
ServeMux baru memberi kontrol pencocokan path yang lebih rinci selain wildcard dasar
{id}... mencocokkan seluruh path lanjutan ke wildcard
{$} mencocokkan akhir path secara ketat
- Menangani aturan prioritas dan konflik antar pola
- Kemungkinan konflik adalah bagian yang sangat diperhatikan dalam proposal ini
- Misalnya, dua pola berikut sama-sama cocok untuk permintaan
/task/0/status/
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
- Dalam kasus ini,
ServeMux baru akan memunculkan panic pada saat pendaftaran
- Pesan error akan menampilkan contoh path yang cocok dengan kedua pola, beserta alasan mengapa tidak ada pola yang lebih spesifik daripada yang lain
- Dalam kode kompleks yang mendaftarkan pola di banyak tempat, diagnosis konflik yang detail seperti ini sangat berguna
Implementasi ulang contoh server task
- Server task/todo-list dari seri REST Servers in Go yang lama diimplementasikan ulang dengan mux yang ditingkatkan di Go 1.22
- Kode lengkap tersedia di contoh stdlib-newmux
- Pola pendaftaran menggabungkan metode HTTP dan wildcard path
mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)
- Seperti pada contoh
gorilla/mux, path yang sama pun bisa dipisahkan ke handler per metode HTTP
- Pada
http.ServeMux sebelumnya, beberapa metode untuk path yang sama akan masuk ke handler yang sama, lalu metode harus diperiksa di dalam handler
- Di mux baru, lebih banyak keputusan routing ditangani di luar handler sehingga pemisahan handler menjadi lebih mudah
Penggunaan PathValue dan keterbatasannya
getTaskHandler membaca ID yang diekstrak dari path dengan req.PathValue("id") lalu mengubahnya menjadi integer dengan strconv.Atoi
- Pola
{id} di mux baru tidak menyediakan regex untuk hanya mencocokkan bilangan bulat, sehingga penanganan error dari strconv.Atoi tetap diperlukan
- Jika konversi ID gagal, respons diberikan dengan
http.StatusBadRequest, dan jika task tidak ditemukan di penyimpanan, respons diberikan dengan http.StatusNotFound
- Implementasi akhirnya sangat mirip dengan solusi Part 2 yang menggunakan
gorilla/mux
- Jika diperlukan, wildcard
{$} pada http.ServeMux baru bisa dipakai untuk membatasi path lanjutan dengan mudah
Posisi mux standar dan router pihak ketiga
- Bagi pemula Go, “paket router mana yang sebaiknya dipakai” adalah pertanyaan yang sering muncul
- Setelah Go 1.22, banyak pengguna mungkin merasa mux standar baru saja sudah cukup tanpa paket pihak ketiga
- Router seperti
gorilla/mux tetap menawarkan lebih banyak fitur dibanding standard library
- Framework ringan seperti Gin tidak hanya menyediakan router, tetapi juga alat tambahan untuk membangun backend web
- Penguatan fitur di standard library adalah perubahan positif baik bagi pengguna paket pihak ketiga maupun pengguna yang hanya memakai standard library
1 komentar
Komentar Hacker News
Menurut saya agak tidak intuitif jika panic terjadi saat dua route cocok pada saat yang sama. Framework web lain biasanya memakai route cocok yang didaftarkan lebih dulu, jadi saya penasaran apakah ada alasan khusus untuk Go
Kasus tepi bahwa “route HTTP bisa didaftarkan dari banyak tempat sehingga sulit menemukan kecocokan ganda” tampaknya bisa diselesaikan dengan tooling. Sepanjang karier saya sering memanfaatkan perilaku “yang cocok lebih dulu menang”, dan ada banyak kebutuhan bisnis yang mengharuskan
/foo/barmenjadi route terpisah, sementara/foo/{id}menjadi route lainProposal desain[1] menyebutkan bahwa mencocokkan pola yang lebih spesifik dapat mempertahankan sifat independen terhadap urutan dari pola ServeMux asli, tetapi bisa jadi sulit melihat sekilas pola mana dari dua pola yang lebih spesifik atau mengapa keduanya bentrok. Karena itu, pesan panic saat mendaftarkan pola yang bentrok dibuat untuk menunjukkan konflik dengan contoh path
Diskusi latar belakang[2] juga menjelaskan bahwa semantik mux tidak bergantung pada urutan pemanggilan
HandleatauHandleFunc. Berkat independensi urutan, urutan inisialisasi paket tidak menjadi penting dan refactoring kode juga lebih mudah. Karena itu, kriteria pemecah seri tidak didasarkan pada urutan pendaftaran; pendaftaran duplikat memicu panic; dan menghindari semantik yang bergantung pada urutan pendaftaran tetap menjadi tujuan desain utama[1]: https://github.com/golang/go/issues/61410
[2]: https://github.com/golang/go/discussions/60227
Baru-baru ini dalam proyek pribadi, saya memakai gorilla/mux dan menemui bug karena tanpa sengaja membuat route yang tumpang tindih. Jika router memicu panic alih-alih berjalan, saya akan terdorong untuk me-refactor URL dan sepenuhnya menghindari bug itu
initper file di GoJika butuh perilaku seperti
/foo/bardan/foo/{id}, buat handler yang menerima path dasar lalu lakukan percabangan eksplisit di dalamnya ke fungsi handler lain. Jika menginginkan perilaku yang tidak ada di standard library, ada banyak library lain dengan fitur lebih banyakStandard library dan bahasa Go selalu cenderung konservatif saat berhadapan dengan kemungkinan ambiguitas, dan berkat itu mudah membangun intuisi yang kuat dan akurat tentang bagaimana standard library bekerja. Kemampuan membaca dan memahami kode dengan cepat dan tepat jauh lebih bernilai daripada menghemat beberapa detik saat menulis kode
/foo/bardan/foo/{id}sepertinya akan diizinkan. Yang pertama lebih spesifik sehingga memiliki prioritas, jadi tampaknya tidak masalahNamun jika ada
/foo/{id}/deletedan/foo/bar/{action}, maka/foo/bar/deletecocok dengan keduanya dan tidak ada yang lebih spesifik, jadi sepertinya akan terjadi panic. Ini terasa masuk akal, tetapi prioritas dengan route yang didaftarkan lebih dulu menang mungkin juga bisa lebih baik$di bagian akhirProyek gorilla/mux terasa agak aneh dan membingungkan. Tahun lalu para maintainer mengarsipkan proyek itu, jadi saya pindah ke multiplexer lain bernama gin-gonic
Namun setelah melihatnya disebut dalam tulisan ini dan mengeceknya lagi, ternyata statusnya sudah tidak diarsipkan. Saya tidak tahu apakah ini menimbulkan pertanyaan tentang stabilitas proyek secara keseluruhan, atau justru menjadi bukti bahwa open source sebenarnya stabil karena ketika developer mundur, orang lain bisa maju dan melanjutkan pemeliharaan. Bagaimanapun, bagus bahwa fitur ini disediakan langsung oleh Go
[0] https://gorilla.github.io/blog/2023-07-17-project-status-upd...
[1] https://www.reddit.com/r/golang/comments/1528e25/gorilla_web...
[2] https://news.ycombinator.com/item?id=36935541
Sebelumnya saya juga pernah menulis ini di komentar[1], tapi menurut saya sintaks proposalnya keliru
Untuk mendefinisikan handler, kita harus membuat string ajaib yang aneh. Saya tidak mengerti kenapa tidak dijadikan argumen sungguhan saja; dengan begitu konstanta yang sudah ada juga bisa dipakai lebih mudah
[1] https://github.com/golang/go/issues/61410#issuecomment-16580...
Kalau menambahkan argumen baru ke antarmuka Mux, kode yang ada akan rusak, dan karena signature metode tidak bisa diubah, string ajaib seperti ini terlihat seperti kompromi yang masuk akal. Metode HTTP juga tidak akan berubah dalam waktu dekat, dan mudah divalidasi saat registrasi atau lewat analisis statis. Saya kurang melihat nilai apa yang diberikan penggunaan konstanta, apalagi jika harus merusak kompatibilitas mundur atau harus selamanya menjalankan API mux baru yang hanya sedikit berbeda dari API yang ada
{parameter}, bukankah ini memang sudah string ajaib?Dengan istilah RFC2616, anggap saja
Request-URIdiberi sedikit keajaiban parameter, lalu ditambah sesuatu yang terlihat seperti request line menjadi[ Method SP ] Request-URI. Ini sepenuhnya kompatibel mundur, dan hampir jelas apa fungsinya meski tanpa membaca dokumentasiMethodadalah salah satu dari beberapa konstanta yang sudah didefinisikan atauextension-method = token, dantokenmengecualikan spasi, slash, serta berbagai tanda kurung, jadi bahkan metode kustom yang sangat aneh pun tampaknya tidak mungkin membingungkan atau salah diparse.Get()dan.Post()tidak pernah benar-benar menyelesaikan sesuatu bagi saya. Metode registrasi tunggal yang memasukkan metode ke dalam string mungkin sepenuhnya baik-baik sajaHEADsecara otomatisSaya tidak suka pendekatan ini. Saya penasaran apakah ada alasan kenapa memakai prefiks metode yang dijadikan string
Type safety dari metode per verba seperti
mux.Getdanmux.Postlebih baik daripada string ajaib yang divalidasi saat runtime. Autocomplete editor atau IntelliSense juga jadi memungkinkanfunc Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }Tipe sengaja dihilangkan agar terlihat sederhana
Menarik. Karena sekarang mux bisa mencocokkan sampai metode, saya sempat penasaran apa yang terjadi ketika route cocok tetapi metodenya tidak. Apakah 404 atau 405 yang jadi masalahnya
Setelah dicek, yang dikembalikan adalah 405 dengan header
Allowyang terisi dengan benarhttps://cs.opensource.google/go/go/+/master:src/net/http/ser...
Saya tahu banyak orang tidak suka antarmuka berbasis string, tetapi menurut saya orang lebih mungkin memasukkan sesuatu yang benar tetapi tidak sesuai daripada salah ketik nama metode HTTP. Secara pribadi, cukup jika analisis statis memperingatkan sintaks yang salah
Namun kalau sudah punya kebutuhan tingkat lanjut, menurut saya lebih baik tidak memakai serve mux bawaan. Ada banyak pilihan yang lebih cocok untuk berbagai use case, dan kalau Anda sudah membuat route secara dinamis, mungkin lebih sedikit repot memakai router sendiri daripada memaksa struktur data yang Anda punya agar cocok dengan router yang ada
Untuk path yang tumpang tindih, akan jauh lebih baik jika dicocokkan sesuai urutan definisi, bukan panic
Pada contoh, jika path
/task/0/{action}/didefinisikan sebelum path wildcard, saya akan berharap path itu yang dicocokkan lebih dulu. Dengan begitu handler untuk kasus khusus bisa didefinisikan dengan mudahDaripada gagal dengan cara yang “membantu”, menurut saya lebih baik ia bekerja begitu saja seperti yang saya katakan. Baunya agak aneh dan tidak terasa seperti Go
ServeMux asli memang dirancang untuk tidak menghormati urutan registrasi, karena bergantung pada urutan registrasi itu berbahaya. Salah satu tujuan desain utama Go adalah mendukung “pemrograman dalam skala besar”, dan efek samping aneh akibat perubahan kode yang berjauhan persis jenis hal yang ingin dihindari
Dalam contoh sederhana ketika semua registrasi dilakukan dalam satu fungsi di satu paket, maksud urutan registrasinya jelas. Namun di codebase yang cukup besar, registrasi kemungkinan terjadi di banyak tempat, dan bisa juga berasal dari file input atau hasil kode yang digenerasi. Orang yang menulis spesifikasi kode hasil generasi mungkin tidak tahu seluk-beluk urutan eksekusi dan dampaknya pada routing. Sekadar berubahnya pengurutan leksikografis import paket pun bisa membuat routing berubah tak terduga
Menghindari hasil yang tak terduga, memudahkan refactoring, dan membuat program kompleks lebih mudah dinalar jelas merupakan arah yang Go banget. Kita tidak harus selalu setuju dengan pilihannya, tetapi alasannya konsisten
Senang karena satu dependensi eksternal berkurang. Sebagai tambahan, kombinasi sqlc + Postgres + templ + htmx + Tailwind adalah stack dengan produktivitas pengembangan yang sangat tinggi. templ terasa seperti JSX untuk Go
Saya juga harus mencoba melihat htmx
Saya sempat berharap bud dengan kompilasi Svelte akan mengisi celah ini, tetapi templ terlihat seperti alternatif yang lebih baik
Saya benar-benar tidak suka usulan ini. Memasukkan metode permintaan HTTP ke dalam URI, itu pun hanya kadang-kadang, dan mungkin juga menyarankan sesuatu seperti
POST,PUT,PATCH /something—saya lebih baik menolakKalau memang harus, cukup buat metode khusus yang menerima nama metode permintaan HTTP
Saya penasaran masalah apa yang diselesaikan oleh metode khusus yang menerima nama metode HTTP dibandingkan memasukkan metode ke dalam string
Seharusnya bisa ada argumen terpisah untuk daftar metode atau opsi
Go bukan alat utama saya, tapi saya pernah memakai Gin, dan bentuknya seperti ini:
router.GET("/", func(context *gin.Context) { ... })Sepertinya tidak jauh berbeda dari berbagai bahasa dan framework lain. Saya penasaran apakah ada contoh lain yang memakai format
GET /path/Tim bahasa resmi memakai string berprefiks alih-alih enum tampak seperti pilihan desain yang sangat aneh
Misalnya,
(Http.GET, "/path")terlihat lebih baik daripada("GET /path")yang sekarang. AnehMeski begitu, saya lebih suka ada beberapa fungsi seperti
mux.GET,mux.POSTdaripada cara tersebut