1 poin oleh GN⁺ 2023-10-17 | 1 komentar | Bagikan ke WhatsApp
  • 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

 
GN⁺ 2023-10-17
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/bar menjadi route terpisah, sementara /foo/{id} menjadi route lain

    • Karena tidak bisa dijamin bahwa urutan pendaftaran selalu sama, pada dasarnya itu menjadi perilaku tak terdefinisi. ServeMux memang sejak awal dirancang dan diimplementasikan begitu, dan mempertahankan dukungan untuk perilaku itu dianggap berguna
      Proposal 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 Handle atau HandleFunc. 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
    • Alasan memakai bahasa terkompilasi dan bertipe seperti Go adalah untuk menggeser bug lebih ke kiri. Bug yang tertangkap saat aplikasi mulai berjalan lebih baik daripada bug yang meledak sebagai kasus pojok di layanan nyata, dan bug yang tertangkap saat kompilasi lebih baik daripada bug yang tertangkap saat startup
      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
    • panic itu sendiri adalah alat. Ia mencegah kita memakai kode yang ambigu dan sulit disimpulkan, serta mencegah ketergantungan pada faktor yang bisa berubah secara arbitrer seperti urutan inisialisasi. Misalnya, sekadar mengganti nama file saja bisa memengaruhi urutan fungsi init per file di Go
      Jika butuh perilaku seperti /foo/bar dan /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 banyak
      Standard 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/bar dan /foo/{id} sepertinya akan diizinkan. Yang pertama lebih spesifik sehingga memiliki prioritas, jadi tampaknya tidak masalah
      Namun jika ada /foo/{id}/delete dan /foo/bar/{action}, maka /foo/bar/delete cocok 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
    • Mendapat error jauh lebih baik untuk debugging daripada menerima data yang salah karena route yang benar-benar dijalankan ternyata route lain. Saya pernah benar-benar mengalami hal seperti ini di Django akibat route yang lupa regex $ di bagian akhir
  • Proyek 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

  • 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...

    • Saya rasa itu karena jaminan kompatibilitas mundur Go
      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
    • Karena parsing {parameter}, bukankah ini memang sudah string ajaib?
      Dengan istilah RFC2616, anggap saja Request-URI diberi 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 dokumentasi
      Method adalah salah satu dari beberapa konstanta yang sudah didefinisikan atau extension-method = token, dan token mengecualikan spasi, slash, serta berbagai tanda kurung, jadi bahkan metode kustom yang sangat aneh pun tampaknya tidak mungkin membingungkan atau salah diparse
    • Saya setuju ini agak aneh, tapi alasannya jelas. Mereka tidak ingin merusak atau mengubah antarmuka publik yang sudah ada
    • Awalnya saya merasakan hal yang sama, tapi jujur saja saat memakai Gin, metode seperti .Get() dan .Post() tidak pernah benar-benar menyelesaikan sesuatu bagi saya. Metode registrasi tunggal yang memasukkan metode ke dalam string mungkin sepenuhnya baik-baik saja
    • Saya tidak tahu apa masalahnya dengan mendaftarkan route HEAD secara otomatis
  • Saya tidak suka pendekatan ini. Saya penasaran apakah ada alasan kenapa memakai prefiks metode yang dijadikan string
    Type safety dari metode per verba seperti mux.Get dan mux.Post lebih baik daripada string ajaib yang divalidasi saat runtime. Autocomplete editor atau IntelliSense juga jadi memungkinkan

    • Hal seperti ini sangat mudah ditambahkan. Kalau memang sebegitu mengganggu, cukup buat seperti func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
      Tipe sengaja dihilangkan agar terlihat sederhana
    • Saya juga kurang suka. Saya ingin yakin bahwa routing bekerja pada waktu kompilasi, bukan runtime
    • Kalau ingin type safety, pilih bahasa yang type-safe
    • Saya tidak lebih suka penggunaan string, tapi kalau adil, saat request masuk metode HTTP pada akhirnya juga string. Ada keindahan karena ini cocok dengan prefiks baris pertama paket HTTP
    • Hal ini dibahas cukup luas dalam proposalnya
  • 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 Allow yang terisi dengan benar
    https://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 mudah
    Daripada 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

    • Semantik bahasa itu penting, dan ini sebenarnya pilihan yang cukup Go banget
      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

    • Sejauh ini pengalaman saya memakai sqlc cukup bagus
      Saya juga harus mencoba melihat htmx
    • Saya penasaran bagaimana Tailwind ditangani di sini. Ide menambahkan Tailwind memang sangat bagus, tetapi kalau harus memakai npm, saya lebih memilih menulis CSS sendiri
    • Awalnya saya kira itu typo dari template, tetapi ternyata memang ada paket bernama templ yang berbeda dari standard library. Setelah melihat sekilas dokumentasinya, kelihatannya cukup rapi
      Saya sempat berharap bud dengan kompilasi Svelte akan mengisi celah ini, tetapi templ terlihat seperti alternatif yang lebih baik
    • Akan sangat bagus kalau templ punya dukungan IntelliJ. Saya suka idenya
  • 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 menolak
    Kalau memang harus, cukup buat metode khusus yang menerima nama metode permintaan HTTP

    • Entah kenapa. Sebenarnya tidak ada perbedaan besar. Saya rasa kita tidak bisa benar-benar membenci seluruh usulan hanya karena satu masalah gaya sederhana
      Saya penasaran masalah apa yang diselesaikan oleh metode khusus yang menerima nama metode HTTP dibandingkan memasukkan metode ke dalam string
    • Setuju. Desain seperti itu kurang masuk akal
      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/

    • Selain kompatibilitas ke belakang, tidak ada alasan untuk melakukannya seperti ini, jadi mungkin hampir tidak ada
    • Saya belum pernah melihatnya dalam praktik. Itu memang cara yang buruk
  • 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. Aneh

    • Ini cara untuk menjaga janji kompatibilitas Go 1
    • Sebagai pengembang Go, dalam kasus seperti ini saya berharap ada function overloading demi kompatibilitas ke belakang. Janji kompatibilitas itu bagus, tetapi keanehan yang ditimbulkannya kurang menyenangkan
    • Ini kesalahan yang tidak bisa diperbaiki tanpa merusak kode lama atau memperkenalkan fungsi baru
      Meski begitu, saya lebih suka ada beberapa fungsi seperti mux.GET, mux.POST daripada cara tersebut