1 puan yazan GN⁺ 2023-10-17 | 1 yorum | WhatsApp'ta paylaş
  • Go 1.22’deki net/http ServeMux iyileştirmesi, standart HTTP çoklayıcısına daha güçlü desen eşleme ekleyerek, yalnızca basit yol eşleme nedeniyle üçüncü taraf router’lara bağımlı olunan alanları azaltıyor
  • Yeni mux, desen içinde HTTP metodu ile /task/{id}/ gibi yol jokerlerini birlikte kullanabildiği için, aynı yolu GET, POST, DELETE gibi metodlara göre farklı handler’lara bağlayabiliyor
  • Eşleşen değerler req.PathValue("id") ile okunuyor; {id}... ve {$} sayesinde de takip eden yolun tamamı ya da tam yol sonu üzerinde kontrol sağlanabiliyor
  • Desen çakışmaları, kayıt anında oluşan panic ile ortaya çıkıyor; hangi yolun iki tarafa da eşleştiğini ve neden hiçbir tarafın daha spesifik olmadığını da gösteriyor
  • Yalnızca standart mux ile yeterli olan sunucuların sayısı artabilir, ancak gorilla/mux ya da Gin gibi üçüncü taraf router ve framework’ler hâlâ daha geniş özellikler ve araçlar sunuyor

Go 1.22’deki ServeMux değişiklikleri

  • Go 1.22 ile birlikte, net/http içindeki varsayılan HTTP sunum çoklayıcısı http.ServeMux için güçlendirilmiş desen eşleme geliyor
  • Mevcut http.ServeMux, temel olarak basit yol eşleme odaklı olduğundan, daha karmaşık yönlendirme ihtiyaçlarında üçüncü taraf kütüphaneler sıkça kullanılıyordu
  • Yeni mux, gelişmiş eşleme yeteneklerini standart kütüphaneye taşıyarak üçüncü taraf paketlerle arasındaki işlev farkını azaltıyor
  • Karşılaştırma için, mevcut standart kütüphane yaklaşımı ile gorilla/mux kullanan bir REST sunucusu örneği kullanılıyor

Yeni mux’un temel kullanım biçimi

  • Daha önce gorilla/mux gibi Go router paketleri kullanmış geliştiricilere, yeni standart mux’un kullanım şekli tanıdık gelecektir
  • Örnekte http.NewServeMux() ile bir mux oluşturuluyor ve HandleFunc ile daha zengin desenler kaydediliyor
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)
})
  • İlk handler, desene GET metodunu dahil ettiği için yalnızca /path/ ile başlayan GET isteklerinde çalışıyor
  • İkinci handler, /task/{id}/ içindeki {id} ile tek bir yol bileşenini eşliyor; handler içinde de değer r.PathValue("id") ile okunuyor
  • Go 1.22 henüz yayımlanmamışken hazırlanmış bir örnek olduğu için, gotip ile çalıştırılması öneriliyor
  • Test sonuçları yönlendirme kurallarını doğrudan gösteriyor
    • /what/ isteği 404 page not found döndürüyor
    • /path/ için GET isteği got path döndürüyor
    • /path/ için POST isteği Method Not Allowed döndürüyor
    • /task/f0cd2e/ isteği id=f0cd2e olarak işleniyor

Ek desen özellikleri ve çakışma yönetimi

  • Yeni ServeMux, temel joker desteğinin ötesinde yol eşlemeyi daha ayrıntılı kontrol etme imkânı sunuyor
    • {id}... ile takip eden yolun tamamını bir jokerle eşleyebiliyor
    • {$} ile yol sonunu sıkı biçimde eşleyebiliyor
    • Desenler arasındaki öncelik ve çakışma kurallarını işleyebiliyor
  • Çakışma olasılığı, öneride özellikle önem verilen konulardan biri
  • Örneğin aşağıdaki iki desen de /task/0/status/ isteğine eşleşiyor
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
  • Bu durumda yeni ServeMux, kayıt anında panic oluşturuyor
  • Hata mesajı, iki desenin birlikte eşlediği örnek yolu ve neden hiçbir desenin diğerinden daha spesifik olmadığını birlikte gösteriyor
  • Desenlerin birden fazla yerde kaydedildiği karmaşık kodlarda, bu tür ayrıntılı çakışma teşhisi özellikle yararlı oluyor

task sunucusu örneğinin yeniden uygulanması

  • Mevcut REST Servers in Go serisindeki task/todo-list sunucusu, Go 1.22’nin geliştirilmiş mux’u ile yeniden uygulanıyor
  • Tüm kod stdlib-newmux örneğinde yer alıyor
  • Kayıt desenleri, HTTP metodunu ve yol jokerlerini birlikte kullanıyor
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)
  • gorilla/mux örneğinde olduğu gibi, aynı yol HTTP metoduna göre farklı handler’lara ayrılabiliyor
  • Önceki http.ServeMux sürümünde, aynı yolun farklı metodları aynı handler’a gider ve metod kontrolünün handler içinde yapılması gerekirdi
  • Yeni mux ile yönlendirme kararı daha çok handler dışına taşındığı için handler’ları ayırmak kolaylaşıyor

PathValue kullanımı ve sınırlamalar

  • getTaskHandler, yoldan çıkarılan ID’yi req.PathValue("id") ile okuyor ve strconv.Atoi ile tam sayıya dönüştürüyor
  • Yeni mux’taki {id} deseni yalnızca tam sayıları eşleyen bir regex belirtmediği için, strconv.Atoi hata kontrolü gerekiyor
  • ID dönüştürme başarısız olursa http.StatusBadRequest, depoda task bulunamazsa http.StatusNotFound döndürülüyor
  • Nihai uygulama, gorilla/mux kullanılan Part 2 çözümüne oldukça benziyor
  • Gerekirse yeni http.ServeMux içindeki {$} jokeriyle takip eden yolu kolayca sınırlandırmak mümkün

Standart mux ile üçüncü taraf router’ların konumu

  • Go’ya yeni başlayanlar için “hangi router paketini kullanmalıyım?” sorusu sıkça soruluyordu
  • Go 1.22 sonrasında birçok kullanıcı, üçüncü taraf paket kullanmadan da yalnızca yeni standart mux ile ihtiyaçlarının karşılandığını düşünebilir
  • gorilla/mux gibi router’lar, standart kütüphaneye göre hâlâ daha fazla özellik sunuyor
  • Gin gibi hafif framework’ler ise yalnızca router değil, web backend geliştirmek için ek araçlar da sağlıyor
  • Standart kütüphanenin güçlenmesi, hem üçüncü taraf paket kullananlar hem de yalnızca standart kütüphane kullananlar için olumlu bir gelişme

1 yorum

 
GN⁺ 2023-10-17
Hacker News yorumları
  • İki route aynı anda eşleştiğinde panic üretmesi sezgilere aykırı gibi geliyor. Diğer web framework’leri genelde önce kaydedilen eşleşen route’u kullanır; bunun Go’ya özgü bir nedeni olup olmadığını merak ediyorum.
    “HTTP route’ları birden fazla yerde kaydedilebildiği için çakışan eşleşmeleri bulmak zor” şeklindeki uç durum araçlarla çözülebilir gibi görünüyor. Kariyerim boyunca “ilk eşleşen kazanır” davranışından çok yararlandım; ayrıca /foo/bar’ın ayrı bir route, /foo/{id}’nin başka bir route olması gereken pek çok iş gereksinimi de vardı.

    • Kayıt sırasının her zaman aynı olacağı garanti edilemez; bu yüzden fiilen tanımsız davranış haline gelir. ServeMux baştan böyle tasarlanıp uygulanmıştı ve bu davranışı desteklemeyi sürdürmenin yararlı olduğu düşünülmüş.
      Tasarım önerisinde[1], daha spesifik örüntünün eşleştirilmesiyle eski ServeMux örüntülerinin sıradan bağımsızlığı korunabilir deniyor; ancak iki örüntüden hangisinin daha spesifik olduğunu ya da neden çakıştıklarını ilk bakışta görmek zor olabilir. Bu yüzden çakışan örüntü kaydedildiğinde çıkan panic mesajı, çakışmayı örnek bir path ile göstermeye ayarlanmış.
      Arka plan tartışmasında[2] da mux semantiğinin Handle veya HandleFunc çağrılarının sırasına bağlı olmadığı açıklanıyor. Sıradan bağımsızlık sayesinde paket başlatma sırası önemli olmaz ve kod refactoring’i de kolaylaşır. Bu nedenle eşitlik durumunda ölçüt olarak kayıt sırasını kullanmamak, yinelenen kayıtların panic üretmesi ve kayıt sırasına bağlı semantikten kaçınmak temel tasarım hedefi olarak kalmış.
      [1]: https://github.com/golang/go/issues/61410
      [2]: https://github.com/golang/go/discussions/60227
    • Go gibi derlenen ve tipli bir dil kullanmanın nedeni, hataları daha sola çekmektir. Gerçek serviste köşe durumlarda patlayan bir hatadansa uygulama başlarken yakalanan hata daha iyidir; başlarken yakalanan hatadansa derleme zamanında yakalanan hata daha da iyidir.
      Yakınlarda kişisel bir projede gorilla/mux kullanırken yanlışlıkla çakışan route’lar oluşturduğum bir hatayla karşılaştım; router çalışmak yerine panic üretseydi, URL’leri yeniden düzenlemeye zorlanır ve hatayı tamamen önlemiş olurdum.
    • panic’in kendisi bir araçtır. Belirsiz ve akıl yürütmesi zor kodu kullanmayı engeller; başlatma sırası gibi keyfi şekilde değişebilecek unsurlara bağımlı kalmamanızı sağlar. Örneğin yalnızca dosya adını değiştirmek bile Go’nun dosya bazındaki init fonksiyonlarının sırasını etkileyebilir.
      /foo/bar ve /foo/{id} gibi bir davranış gerekiyorsa, temel path’i alan bir handler yazıp onun içinde diğer handler fonksiyonlarına açıkça dallanabilirsiniz. Standart kütüphanede olmayan bir davranış istiyorsanız, daha fazla özelliğe sahip pek çok başka kütüphane de var.
      Go standart kütüphanesi ve dili, olası belirsizlikler karşısında her zaman muhafazakâr tarafa eğilimli oldu; bu da standart kütüphanenin nasıl davrandığına dair güçlü ve doğru bir sezgi oluşturmayı kolaylaştırıyor. Kod yazarken birkaç saniye kazanmaktansa onu hızlı ve doğru biçimde okuyup anlayabilmek çok daha değerlidir.
    • /foo/bar ile /foo/{id}’ye izin verilecek gibi duruyor. İlki daha spesifik olduğu için önceliğe sahip olur; bu yüzden sorun yok gibi görünüyor.
      Ancak /foo/{id}/delete ve /foo/bar/{action} varsa, /foo/bar/delete ikisiyle de eşleşir ve hiçbiri daha spesifik olmaz; bu durumda panic çıkacak gibi. Mantıklı hissettiriyor, ama önce kaydedilenin kazandığı bir öncelik daha iyi de olabilir.
    • Yanlış veri almaya, çünkü gerçekte çalışan route başka bir route çıktı, kıyasla hata almak debug açısından çok daha iyidir. Django’da sondaki $ regex’i eksik olan bir route yüzünden gerçekten böyle bir şey yaşamıştım.
  • gorilla/mux projesi biraz tuhaf ve kafa karıştırıcı. Geçen yıl bakımcılar projeyi arşivlediği için gin-gonic adlı başka bir multiplexer’a geçmiştik.
    Ama yazıda adı geçince tekrar kontrol ettim ve artık arşiv durumunda olmadığını gördüm. Bunun projenin genel kararlılığı hakkında soru işareti mi yarattığını, yoksa geliştiriciler ayrılsa bile başkalarının devreye girip bakımı sürdürebilmesi açısından açık kaynağın gerçekten istikrarlı olduğunun kanıtı mı olduğunu bilmiyorum. Yine de bu özelliğin Go’nun kendisinde sunulması güzel.

  • Bunu daha önce yorumda[1] da yazmıştım ama önerinin sözdiziminin yanlış olduğunu düşünüyorum
    Bir handler tanımlamak için tuhaf sihirli string’ler oluşturmak gerekiyor. Neden bunu gerçek argüman yapmadıklarını bilmiyorum; öyle olsa mevcut sabitler de daha kolay kullanılabilirdi
    [1] https://github.com/golang/go/issues/61410#issuecomment-16580...

    • Bence bu Go’nun geriye dönük uyumluluk garantisi yüzünden olabilir
      Mux arayüzüne yeni bir argüman eklemek mevcut kodu bozar; metot imzasını değiştiremeyince bu tür sihirli string’ler makul bir uzlaşma gibi görünüyor. HTTP metotları yakın zamanda değişecek de değil; kayıt anında ya da statik analizle doğrulaması da kolay. Sabit kullanmanın nasıl bir değer kattığını pek bilmiyorum; özellikle de geriye dönük uyumluluğu bozacaksa veya mevcut API’den çok az farklı yeni bir mux API’sini sonsuza dek paralel götürmek gerekecekse.
    • {parameter} ayrıştırması yüzünden zaten sihirli string değil mi?
      RFC2616 terimleriyle Request-URI üzerine biraz parametre sihri eklenmiş gibi düşünüp, bunun istek satırına benzeyen [ Method SP ] Request-URI haline geldiğini varsayabiliriz. Tamamen geriye dönük uyumlu ve dokümana bakmadan bile ne yaptığı neredeyse açık
      Method ya önceden tanımlı birkaç sabitten biri ya da extension-method = token; token ise boşluk, slash ve çeşitli parantezleri dışladığı için çok garip bir özel metotta bile karışma veya yanlış ayrıştırılma ihtimali yok gibi görünüyor
    • Biraz tuhaf olduğuna katılıyorum ama nedeni açık: mevcut public arayüzü kırmak veya değiştirmek istemiyorlar
    • Başta ben de aynı histeydim ama açıkçası Gin kullanırken .Get(), .Post() gibi metotların bir şeyi çözdüğünü hiç görmedim. Metodu string’in içine koyan tek bir kayıt metodu da muhtemelen tamamen iyi olur
    • HEAD route’unu otomatik kaydetmenin nesi sorun, bilmiyorum
  • Bu yaklaşımı sevmiyorum. Neden stringleştirilmiş metot öneki kullandıklarını merak ediyorum; bir nedeni var mı?
    Runtime’da doğrulanan sihirli string’ler yerine mux.Get, mux.Post gibi fiile özel metotların tip güvenliği daha iyi. Editör otomatik tamamlama ya da IntelliSense de mümkün olur

    • Bu kadarı çok kolay eklenebilir. Gerçekten bu kadar dert ediyorsan func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) } gibi yazarsın
      Basit görünsün diye tipleri atladım
    • Ben de pek sevmiyorum. Routing’in runtime’da değil compile-time’da çalıştığından emin olmak isterim
    • Tip güvenliği istiyorsan tip güvenli bir dil seçersin
    • String kullanımını tercih etmiyorum ama adil olmak gerekirse istek geldiğinde HTTP metodu da sonuçta string. HTTP paketinin ilk satırındaki önekle birebir uyuşmasının bir güzelliği var
    • Bu konu öneri metninde genişçe ele alınıyor
  • İlginç. Artık mux metoda kadar eşleştirebildiğine göre, route eşleşip metot eşleşmediğinde ne olacağını merak etmiştim. 404 mü 405 mi meselesi
    Kontrol edince Allow header’ı düzgün doldurulmuş bir 405 döndüğünü gördüm
    https://cs.opensource.google/go/go/+/master:src/net/http/ser...
    String tabanlı arayüzden hoşlanmayan çok kişi olduğunu biliyorum ama HTTP metodu adını yanlış yazmaktansa doğru biçimde bambaşka bir şey girme ihtimalinin daha yüksek olduğunu düşünüyorum. Kişisel olarak statik analiz hatalı sözdizimi için uyarı verirse yeterli
    Yine de zaten gelişmiş gereksinimleriniz varsa varsayılan serve mux’ı kullanmamak daha iyi bence. Çeşitli kullanım senaryolarına daha uygun çok seçenek var; zaten dinamik route üretimi yapıyorsanız, elinizdeki veri yapısını mevcut bir router’a zorla uydurmak yerine doğrudan kendi router’ınızı kullanmak daha az uğraştırabilir

  • Çakışan yolların panic etmek yerine tanımlandıkları sırayla eşleşmesi çok daha iyi olurdu
    Örnekte /task/0/{action}/ yolu wildcard yoldan önce tanımlansaydı, önce onun eşleşmesini beklerdim. Özel durumlara yönelik handler’ları kolayca tanımlayabilmek için böyle olmalı
    “Yardımcı” bir şekilde başarısız olmak yerine, söylediğim gibi çalışması daha iyi bence. Biraz kötü kokuyor ve Go’ya pek yakışmıyor

    • Dilin semantiği önemli ve bu aslında epey Go’ya uygun bir tercih
      Orijinal ServeMux, kayıt sırasına saygı duymayacak şekilde tasarlandı; çünkü kayıt sırasına bağımlı olmak tehlikeli. Go’nun temel tasarım hedeflerinden biri “ölçekli programlama”yı desteklemek ve uzaktaki kod değişikliklerinden kaynaklanan tuhaf yan etkiler tam da kaçınmak istediği türden şeyler
      Tüm kayıtların tek bir paketteki tek bir fonksiyon içinde yapıldığı basit örnekte kayıt sırasının niyeti açık. Ama yeterince büyük bir kod tabanında kayıtların birden fazla yerde yapılması olası; hatta girdi dosyalarının veya üretilmiş kodun sonucu da olabilir. Üretilmiş kodun spesifikasyonunu yazan kişi, çalışma sırasının inceliklerini ve bunun routing’e etkisini bilmiyor olabilir. Paket import’larının sözlük sıralamasının değişmesi bile routing’i beklenmedik şekilde değiştirebilir
      Beklenmeyen sonuçları önlemek, refactoring’i kolaylaştırmak ve karmaşık programlar üzerinde akıl yürütmeyi kolaylaştırmak açıkça Go’ya uygun bir yön. Tercihe her zaman katılmak zorunda değiliz ama gerekçe tutarlı
  • Dış bağımlılığın bir tane azalması güzel. Konu dışı olarak sqlc + Postgres + templ + htmx + Tailwind kombinasyonu geliştirme üretkenliği çok yüksek bir stack’ti. templ, Go için JSX gibi hissettiriyor

    • Şimdiye kadar sqlc ile oldukça iyi bir deneyim yaşıyorum
      htmx’e de bir bakmam gerekecek
    • Burada Tailwind’i nasıl ele aldığınızı merak ediyorum. Tailwind ekleme fikri gerçekten iyi ama npm kullanmak gerekiyorsa CSS’i doğrudan yazmayı tercih ederim
    • İlk başta template yazım hatası sandım ama gerçekten templ diye, standart kütüphaneden farklı bir paketmiş. Dokümana kısaca bakınca oldukça temiz görünüyor
      Svelte derlemesi eklenmiş bud’ın bu boşluğu doldurmasını umuyordum ama templ daha iyi bir alternatif gibi görünüyor
    • templ için IntelliJ desteği olsaydı gerçekten harika olurdu. Fikir hoşuma gidiyor
  • Bu öneri hiç hoşuma gitmiyor. HTTP istek metodunu URI’nin içine koymak; üstelik bunu yalnızca bazen yapmak ve belki POST,PUT,PATCH /something gibi şeyler de önermek, kalsın.
    Mutlaka gerekiyorsa HTTP istek metodu adını alan özel bir metot yapılabilir.

    • Nedenini anlamıyorum. Pratikte pek bir şey değişmiyor. Sırf basit bir stil meselesi yüzünden tüm öneriden gerçekten nefret edilebileceğini sanmıyorum.
      HTTP metodu adını alan özel bir metodun, metodu string’in içine koymaya kıyasla hangi sorunu çözdüğünü merak ediyorum.
    • Katılıyorum. Böyle tasarlamak pek mantıklı değil.
      Metot ya da seçenek listesi için ayrı bir argüman konulabilirdi.
  • Go ana aracım değil ama Gin kullandım; şöyle bir şey:
    router.GET("/", func(context *gin.Context) { ... })
    Diğer birçok dil ve framework’ten çok farklı görünmüyor. GET /path/ biçimini kullanan başka örnekler var mı merak ediyorum.

    • Geriye dönük uyumluluk dışında bunu yapmanın bir nedeni yok, bu yüzden muhtemelen pek yoktur.
    • Gerçekte hiç görmedim. Sadece kötü bir yaklaşım.
  • Resmî dil ekibinin enum yerine önekli string kullanması çok tuhaf bir tasarım tercihi gibi görünüyor.
    Örneğin mevcut ("GET /path") yerine (Http.GET, "/path") daha iyi görünüyor. Garip.

    • Go 1 uyumluluk sözünü tutmanın yolu bu.
    • Bir Go geliştiricisi olarak böyle durumlarda geriye dönük uyumluluk için function overloading olmasını isterdim. Uyumluluk sözü harika, ama bunun yarattığı tuhaflıklar pek iyi değil.
    • Mevcut kodu bozmadan ya da yeni bir fonksiyon getirmeden düzeltilemeyecek bir hata.
      Yine de bu yaklaşımdansa mux.GET, mux.POST gibi birden fazla fonksiyon olmasını tercih ederim.