2 puan yazan GN⁺ 2024-02-10 | 1 yorum | WhatsApp'ta paylaş
  • Uzun süre yaşayacak Go HTTP servislerinde açık bağımlılık geçirme, tek yerde toplanmış route’lar ve test edilebilir bir run fonksiyonu kullanıldığında bakım ve doğrulama kolaylaşır
  • Handler’ları sunucu struct’ı metotları yerine gerekli değerleri closure olarak alan http.Handler döndüren fonksiyonlar olarak yazın; ortak middleware’leri sunucu oluşturma ve route kaydı aşamasında birleştirin
  • func main()’i ince tutup run() içine context.Context, argümanlar, ortam erişimi ve standart giriş/çıkışı enjekte etmek kapatma işlemlerini ve test kontrolünü sadeleştirir
  • İstek/yanıt kodlama, doğrulama, middleware adaptörleri ve sync.Once ile gecikmeli başlatma, tekrar eden kodu azaltırken Go’nun standart net/http akışını korur
  • Testlerde tek tek handler’lar yerine gerçek API çağrılarına yakın uçtan uca yaklaşım tercih edilir; her test kendi sunucusunu ayağa kaldırır ve hazır olma durumunu /healthz ya da /readyz ile kontrol eder

Sunucu oluşturma ve servis giriş noktası

  • NewServer constructor’ı, servisin temel http.Handler’ını oluşturan fonksiyon olarak bırakılır
    • Genellikle her servis için bir tane olur ve iç route’lar istekleri ilgili handler’lara yönlendirir
    • Logger, yapılandırma, depo, harici client gibi tüm bağımlılıkları argüman olarak alır
    • Mümkünse http.Handler döndürür; karmaşık durumlarda özel bir tip kullanılabilir
    • Kendi muxer’ını oluşturduktan sonra routes.go içindeki route kayıt fonksiyonuna geçirir
  • Tüm endpoint’ler için ortak gerekli HTTP işlemleri NewServer içinde gruplanır
    • CORS
    • Kimlik doğrulama middleware’i
    • Loglama
    • İzleme ID’si middleware’i
  • Bağımlılık argüman listesi uzasa bile bunları fonksiyon argümanları olarak açıkça belirtme yöntemi tercih edilir
    • Struct alanını eksik bıraktığınızda compiler bunu engelleyemeyebilir; ancak fonksiyon argümanlarında gerekli değer geçilmezse çağrı yapılamaz
    • Uzun argüman listeleri dikey biçimlendirilirse daha okunur olur
    • Belirli bir testte kullanılmayan bağımlılıklar nil geçirilerek kullanılmadıkları sinyali verilir

API yüzeyini routes.go içinde toplamak

  • routes.go, servisin tüm route’larının tek yerden görülebileceği dosya olarak tutulur
    • Her projede API yüzeyine göz atılacak tek bir konum oluşur
    • NewServer’ın büyük bağımlılık listesi nedeniyle addRoutes içinde de benzer bir argüman listesi oluşabilir
    • Go’nun tip denetimi eksik ya da sırası hatalı argümanları yakalar
  • addRoutes mümkün olduğunca basit ve düz tutulur
    • Hata verebilecek işlemler önce run fonksiyonunda ele alınır
    • Handler kayıt aşamasında mux.Handle, mux.HandleFunc, http.NotFoundHandler gibi routing işlerine odaklanılır
    • Handler’ın kendisinin hata döndürmesi gereken bir tasarım varsa addRoutes da hata döndürebilir

main sadece run’ı çağırsın

  • func main(), run()’ı çağıran; hata varsa stderr’e yazıp anormal şekilde çıkan ince bir fonksiyon olarak tutulur
    • run, context.Context, argümanlar, giriş/çıkış ve ortam erişim fonksiyonu gibi işletim sistemi temel bileşenlerini argüman olarak alır
    • run hata döndürdüğü için normal Go kodundaki gibi hata işleme yapılabilir
  • run’a geçirilebilecek değerlere örnekler:
    • os.Args: program çalıştırma argümanları ve flag ayrıştırma için kullanılır
    • os.Stdin: giriş okumak için
    • os.Stdout: çıktı yazmak için
    • os.Stderr: hata logları yazmak için
    • os.Getenv: ortam değişkenlerini okumak için
    • os.Getwd: geçerli çalışma dizinini almak için
  • signal.NotifyContext, run içinde ayarlanır
    • Ctrl+C gibi kapatma sinyali geldiğinde context iptal edilir
    • run nil döndürürse normal çıkış yapılır
    • Hata döndürürse main hatayı yazdırır ve sıfır olmayan kodla çıkar
  • Global durumdan kaçınıldığında daha fazla testte t.Parallel() kullanılabilir
    • run birden fazla kez çağrılsa da her çalıştırma birbirini etkilemez
    • Flag’ler global flag yerine run içindeki flags.NewFlagSet ile işlenir
    • Ortam değişkenleri gerçek ortamı değiştirmek yerine getenv func(string) string enjekte edilerek kontrol edilir
    • Bu yöntem, t.SetEnv’den farklı olarak paralel testleri kullanmaya devam etmeyi sağlar

Kapatma ve hazır olma durumunu işleme

  • Context servisin tüm katmanlarına iletilmelidir
    • Kapatma sinyali geldiğinde context iptal edilir
    • Uzun süren veya tekrarlı işler ctx.Err() ya da ctx.Done()’ı kontrol edip durur
    • Başka goroutine’ler başlatıldığında da ne zaman durulacağı context ile belirlenir
  • HTTP sunucusu kapanırken zarif biçimde durmak için Shutdown çağrılır
    • Örnekte ayrı bir goroutine içinde ctx.Done() beklenir
    • Kapatma context’ine 10 * time.Second timeout konur
    • Kapatma sırasında hata olursa stderr’e kaydedilir
  • Testlerde sunucunun gerçekten hazır olup olmadığını kontrol etmek için /healthz ya da /readyz endpoint’i bulundurulur
    • Ayrı bir channel ile hazır sinyali oluşturmak da mümkündür, ancak gerçek HTTP isteğiyle kontrol etme yöntemi tercih edilir
    • Hazır olma kontrol döngüsü 200 OK gelene kadar istek yapar
    • Context iptali ya da timeout’a ulaşılırsa hata döndürür
    • Örnek döngü istekler arasında 250ms bekler

Handler oluşturma biçimi

  • Handler fonksiyonları http.Handler ya da http.HandlerFunc’ı doğrudan implement etmek yerine bunları döndürür
    • Örnek: func handleSomething(logger *Logger) http.Handler
    • Handler’a özel closure ortamı oluşturulabilir
    • Başlatılmış değerler istek işlenirken kullanılabilir
  • Paylaşılan verileri yalnızca salt okunur kullanmak güvenlidir
    • Handler bir değeri değiştiriyorsa mutex gibi koruma mekanizması gerekir
    • Program durumunu closure içinde saklamak genellikle önerilmez
  • Bulut ortamlarında instance’ın uzun süre yaşayacağını varsaymak zordur
    • Sunucu kaynak tasarrufu için kapatılabilir veya başka bir nedenle crash olabilir
    • Birden çok instance aynı anda çalışabilir ve istekler tahmin edilmesi zor biçimde dağıtılabilir
    • Gerçek projelerde kalıcı durumu veritabanında ya da ayrı bir depolama API’sinde tutmak daha iyidir

İstek/yanıt kodlama ve doğrulama

  • Her serviste istek gövdesini decode etmek ve yanıt gövdesini encode etmek gerektiğinden encode / decode helper’ları bulundurulur
    • Örnek, JSON Content-Type’ını ayarlar, durum kodunu yazar ve ardından json.NewEncoder(w).Encode(v) çağırır
    • Decode işlemi json.NewDecoder(r.Body).Decode(&v) çağrısını sarar ve hataya bağlam ekler
    • Generics kullanıldığında encode(w, r, http.StatusOK, obj) gibi tip çıkarımı yapılabilir
    • decode dönüş tipine bağlı olduğundan decode[CreateSomethingRequest](https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/r) gibi beklenen tip açıkça belirtilmelidir
  • Doğrulama için tek metotlu bir interface kullanılır
    • Validator interface’i Valid(ctx context.Context) map[string]string biçimindedir
    • Sorun yoksa uzunluğu 0 olan bir map döndürülür
    • Sorunlu alanlar için alan adı key, insan tarafından okunabilir açıklama value olur
  • Doğrulanacak şeyler hızlı alan kontrollerine uygundur
    • Zorunlu alanın boş olup olmadığı
    • E-posta gibi belirli string biçimlerinin uygun olup olmadığı
    • Sayının izin verilen aralıkta olup olmadığı
  • Veritabanı sorgusu gibi daha karmaşık kontroller ayrı bir yerde ele alınır
    • Bu tür kontroller hızlı doğrulama fonksiyonunun içine saklanamayacak kadar önemlidir
    • Generic sürüm decodeValid[T Validator], T tipinin mutlaka Validator implement etmesini zorunlu kılar
    • nil map üzerinde len(problems) çağırmak 0 döndürür, bu yüzden panic oluşmaz

Middleware adaptör paterni

  • Middleware bir http.Handler alıp yeni bir http.Handler döndürür
    • Asıl handler çağrısından önce ve sonra kod çalıştırabilir
    • Koşula bağlı olarak asıl handler’ı hiç çağırmayabilir
    • Örnekteki adminOnly, kullanıcı yönetici değilse HTTP 404 Not Found döndürür ve asıl handler’ı çağırmaz
  • Middleware uygulama yeri genellikle routes.go olur
    • Yalnızca endpoint listesine bakarak hangi route’a hangi middleware’in eklendiği anlaşılır
    • Middleware listesi uzarsa okunurluk için birden çok satıra bölünür
  • Çok bağımlılığı olan middleware, middleware döndüren bir fonksiyonla sarılır
    • newMiddleware(logger, db, slackClient, rroll) bir func(http.Handler) http.Handler döndürür
    • Route kayıt kodunda middleware(handleSomething(...)) gibi kısa kullanılır
    • Ayrı bir type middleware func(h http.Handler) http.Handler tanımlanabilir; ancak dönüş tipini doğrudan yazmak kodu okumayı daha açık hale getirir

İstek/yanıt tiplerinin kapsamını daraltmak

  • Yalnızca belirli bir endpoint’te kullanılan istek/yanıt tipleri handler fonksiyonunun içinde tanımlanabilir
    • Global namespace temiz kalır
    • Diğer handler’ların stabil olduğu garanti edilmeyen tiplere bağımlı olması engellenir
  • Test kodunda aynı tipe ihtiyaç duyulursa sürtünme oluşabilir
    • Bu durumda tipi dışarı almak da makuldür
    • İstek/yanıt tipi handler içinde olduğunda testlerde yeni bir anonim struct ya da yerel tip tanımlanabilir
  • Testteki yerel tip niyeti gösterir
    • Örneğin /greet endpoint’i Person’ın tamamına değil yalnızca Name alanına ihtiyaç duyuyorsa test giriş struct’ında sadece Name bulunur
    • Testi okuyan kişi bu endpoint’in hangi alanla ilgilendiğini hemen anlar

sync.Once ile başlatmayı geciktirmek

  • Handler hazırlanırken maliyeti yüksek işler sync.Once ile ilk istek anına ertelenir
    • Uygulama başlangıç süresi azalır
    • Handler çağrılmazsa maliyetli iş çalıştırılmaz
  • Örnek, template dosyalarının parse edilmesini ilk istekte yalnızca bir kez yapar
    • sync.Once, kodun yalnızca bir kez çalışmasını garanti eder
    • Aynı anda gelen diğer istekler başlatma tamamlanana kadar bekler
    • Hata kontrolü init.Do dışında yapılarak hatanın görünür kalması sağlanır
  • Bu yöntem başlatma süresini uygulama başlangıcından runtime’daki ilk endpoint erişimi anına taşır
    • Google App Engine’in yoğun kullanıldığı ortamlarda bu yöntem uygun olabilir
    • Dağıtım ortamına göre sync.Once’ın nerede ve ne zaman kullanılacağına karar verilmelidir

Test stratejisi

  • Bu yapı test edilebilirliği önemli bir hedef olarak alır
    • run fonksiyonu test kodundan programı doğrudan çalıştırmayı sağlar
    • Testler; program davranışının anlaşılmasını kolaylaştırıp kolaylaştırmadığı, değişiklikte bozulma endişesini azaltıp azaltmadığı ve geçtikten sonra production dağıtımı için güven verip vermediği ölçüt alınarak değerlendirilir
  • Yalnızca handler’lar bağımsız olarak test edilebilir
    • Handler oluşturma fonksiyonu çağrılır ve gerekli bağımlılıklar geçirilir
    • httptest.NewRecorder ve http.NewRequest ile istek ve yanıt oluşturulur
    • Durum kodu, yanıt gövdesi ve header’lar doğrulanır
    • Bu yöntem kimlik doğrulama gibi middleware’leri atlayıp doğrudan handler koduna girer
  • Daha çok tercih edilen yöntem uçtan uca teste yakındır
    • run çağrılarak program gerçek çalıştırma biçimine yakın şekilde ayağa kaldırılır
    • Argüman ayrıştırma, bağımlılık bağlantıları, veritabanı migration’ları ve sunucu başlatma dahil edilir
    • Test API’yi çağırdığında tüm katmanlar ve routes.go da birlikte doğrulanır
    • Gerçek veritabanıyla etkileşime girebilir
  • Bu yöntem tekrarlı testleri azaltmaya yardımcı olur
    • Tüm katmanlar ayrı ayrı test edilirse aynı içerik küçük farklarla birçok kez doğrulanabilir
    • Uçtan uca testler, kullanıcı ile sistem etkileşimini anlatan merkezi bir test seti sağlar
    • TDD vb. ile önceden oluşmuş unit testler uygunsa korunur; ancak uçtan uca testlerle aynı şeyi tekrar ediyorsa silinebilir
  • Her test kendi program instance’ını çalıştırabilir
    • Her test farklı argüman, flag, standart giriş/çıkış ve ortam değişkenleri geçirir
    • context.WithCancel ile cancel fonksiyonu oluşturulur ve t.Cleanup(cancel) içine kaydedilir
    • Test bittiğinde context iptal edilir ve program zarif biçimde kapanır
    • Go 1.14’teki t.Cleanup, doğrudan defer kullanmaya alternatif olarak kullanılır

Gerçek uygulama kapsamı ve organizasyon bağlamı

  • Basit API’ler geliştirirken bu patern, okunması kolay ve genişletilebilir kodu hedefler
    • Paterni kopyalayıp genişletmek kolaydır
    • Yeni kişilerin çalışması kolaydır
    • Değişiklik yaparken endişe azalır
    • Sihirli davranışlar olmadan açıkça yapılandırılır
  • Kod üretim araçları kullanılsa bile bu yöntem sürdürülebilir
    • Örnek olarak template tabanlı boilerplate üretimi için Oto package kullanılabilir
  • Büyük projelerde ya da büyük organizasyonlarda mevcut teknoloji tercihleri kararı değiştirebilir
    • Grafana Labs gibi organizasyonlarda belirli araçlar ve soyutlamalar zaten yaygın biçimde kullanılıyor olabilir
    • gRPC bunun bir örneğidir
    • Yerleşik patern ve deneyimler varsa, bu akışı izlemek pratik bir tercih olur
  • Grafana IRM ürün ailesi bağlamı da dahildir
    • Grafana IRM, Grafana Labs tarafından geliştirilmekte olan bir ürün ailesidir
    • Grafana Alerting, metrikler izin verilen aralığın dışına çıktığında bildirim gönderir
    • Grafana OnCall, nöbet çizelgeleri ve escalation kurallarıyla doğru kişiye ulaşma sürecini otomatikleştirir
    • Grafana Incident, Zoom odası, özel Slack kanalı ve olay zaman çizelgesi oluşturur; olay müdahalesine yardımcı olur
    • Slack kanalında robot yüz emojisiyle tepki verilen öğeler zaman çizelgesine eklenir

1 yorum

 
GN⁺ 2024-02-10
Hacker News yorumları
  • Valid metodu gibi ayrı bir doğrulayıcı kullanma yaklaşımını da denedim, ancak Lexi Lambda’nın “Parse, Don’t Validate” [0] yazısını okuduktan sonra Go’nun tip denetleyicisinden yararlanmanın çok daha az hataya yol açtığını hissettim
    Örneğin kullanıcının içinde köşeli parantez bulunan geçersiz bir kullanıcı adını asla belirleyememesini istiyorsanız, doğrulayıcı yaklaşımında kullanıcı adının güvenilmeyen girdiden geldiği tüm kod yollarında doğrulayıcıyı çağırmanız gerekir
    Bunun yerine Username tipi ve NewUsername(username string) (Username, error) kurucusu tanımlarsanız, bir Username nesnesinin var olması bile zaten doğrulamadan geçtiğine dair güvence sağlar
    [0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

    • Tip sistemini doğru kullanınca kodun iyileşmesi yeniden şaşırtıcı geliyor. Her şeyi string olarak geçirmek yerine parse edip tipe bağlamak gerekir
    • İyi bir tasarım deseni, ancak çok erken doğrulama konusunda dikkatli olmak gerekir
      Bu desen doğrulamayı istediğiniz kadar erken ya da geç yapmanıza imkân tanır, ama ne zaman yapmanız gerektiğini söylemez. Genellikle bunu daha büyük bir nesneyi parse etme/doğrulama sürecinin bir parçası olarak yapmak çoğu zaman en iyisidir
      UI bağlamında doğrulanmamış verilerle çalışma fikri için Steven Witten’ın “I is for Intent” [1] yazısı başvurulmaya değer
      [1] https://acko.net/blog/i-is-for-intent/
    • Kavramsal olarak eski private kurucu ve factory metodu tekniğiyle aynı
    • İlgili bağlantılar: Parse, don't validate (2019) - https://news.ycombinator.com/item?id=35053118 - Mart 2023, Parse, Don't Validate (2019) - https://news.ycombinator.com/item?id=27639890 - Haziran 2021, Parse, Don’t Validate - https://news.ycombinator.com/item?id=21476261 - Kasım 2019, Parse, Don't Validate - https://news.ycombinator.com/item?id=21471753 - Kasım 2019
    • Go’da bu deseni uygulamak zordu. Username bir struct’ın içinde yer alıyorsa ve değeri ayarlamayı unutursanız, kısıtı ihlal edebilecek bir zero value girer
  • Tüm sistem ayarlarını temsil eden bir Config nesnesini alıp oraya buraya değiştirilebilir şekilde geçirme yöntemi en sevmediğim desenlerden biri
    Böyle yapınca her şey ayar nesnesi üzerinden birbirine bağlanıyor. Bazı sistemlerde biri kendisine geçirilen ayar nesnesine tekrar değer yazıyordu; düzgün çalışması için her parçanın belirli bir sırayla ayarlanması gerekiyordu
    Başka bir durumda bir alt sistem, daha sonra okuyacağı veriyi ayar nesnesine yazıyordu; bu yüzden sistemin bir bölümünü devre dışı bırakmak mümkün olmuyordu
    “Ayarların tek büyük değiştirilebilir değer olması” deseni yalnızca Go’da değil, diğer dillerde de epey can sıkıcı bir desen

    • Buradaki kilit nokta genel bir ayar veri nesnesi değil, değiştirilebilir ayar nesnesi
      Python projelerinde immutable ayar dataclass’larını çok kullanıp birçok modüle geçiriyoruz; birden fazla fonksiyon birden fazla değere bağımlı olduğunda her birini fonksiyon argümanı olarak geçirmek ve tipleri ayrı ayrı tanımlamak yerine, tüm değişkenlerin ve tip tanımlarının tek bir dataclass içinde bulunması oldukça kullanışlı bir tasarım deseni oluyor
    • Bunu engellemenin en sevdiğim yolu, ayarları gerçekten immutable yapmak ama Option fonksiyonlarıyla oluşturulabilir kılmak
      İçteki options yalnızca oluşturma sırasında değiştirilir, dışarıya ise sadece Config ve erişim metotları açılır. Örneğin config.New(config.Name("Emanon")) gibi oluşturup cfg.Name() ile okuyabilirsiniz
    • Her paket için bir Config struct’ı oluşturup, configs.Config’i de her paketin Config’lerini bir araya getiren bir yapı yapmak eğilimindeyim
      Bu Go best practice’i olmayabilir, ancak başlangıçta tüm sistem ayarlarını tek bir varlık hâline getirebilmek ve her pakete yalnızca ihtiyaç duyduğu asgari bağımlılığı geçebilmek güzel
      Test ederken de yalnızca bir paketi test etmek için tüm ayarları taklit etmek zorunda kalmadığınızdan biraz kolaylaşıyor
    • Katılıyorum. Bir zamanlar en üst düzey ayar nesnesini değiştirip ciddi bir kesintiye yol açmıştım
      Kesinlikle değiştirilmemeli. Nerede nasıl kullanıldığını bilemediğiniz için boşa harcanan iş gücüne yazık; değişiklik gerekiyorsa orijinalden türetilmiş bir değer oluşturmak daha iyi
      Komik olan şu ki tasarım gereği ayar nesnesi bir ölçüde immutable’dı ve değiştirmek için WARNING_DO_NOT_USE API’sini kullanmak gerekiyordu; yine de onunla nesneyi değiştirip kesintiye yol açmıştım
    • Bence yerinde bir eleştiri. Daha kullanışlı desenin ne olduğunu düşündüğünüzü merak ediyorum
  • Mat Ryer’ın çalışmalarını gerçekten seviyorum ve bu yazının 2018 sürümündeki fikirlerin çoğunu o zamandan beri tüm Go projelerimde uyguladım.
    Ancak NewServer’ın tüm bağımlılıkları argüman olarak alan büyük bir constructor olması ve testlerde ihtiyaç duyulmayan bağımlılıkların kullanılmadığını belirtmek için nil geçirilmesi kısmı içime hep sinmedi.
    Bunun sonucu olarak kodun büyük bir bölümü gereksiz yere çok fazla paylaşılan durum taşıyor. Gerçekte çoğu HTTP handler’ı, istekte bulunan kullanıcının kaynağa erişip erişemeyeceğini kontrol edip veri deposundaki tek bir fonksiyonu çağırmakla yetinse de, üst sunucunun sahip olduğu tüm nesnelere ve tüm veri deposuna erişebilen dev bir parçanın parçası hâline geliyor.
    Yalnızca iki metodu mock’layarak test etmek isteseniz bile basit bir test yazmak zorlaşıyor; Mat Ryer’ın kalıbı şu ana kadar gördüklerimin en iyisi olsa da daha iyi bir çözüm olmalı hissi kalıyor.

    • Üzerinde çalıştığım depoda bu tür yüksek gelen bağımlılık birleşimi noktalarına giderek daha duyarlı hâle geldim; özellikle Bazel dünyasına derinleştikçe bağımlılık yönetimi ve fiziksel tasarımın yazdığınız kod üzerindeki etkisi büyüyor.
      Mümkünse plugin’ler iyi bir kod sınırı stratejisidir. Plugin mimarisi temelde varsayılan olarak kapalıdır ve ancak açıkça seçilirse görünür hâle gelir; bu yüzden tüm olasılıkları belirli bir kod parçasına dayatmaz.
      Bu özellikteki yazılıma “alakart” diyorum. Genel olarak “herhangi bir şeyi yapmak için her şeyi yapma” durumundan kaçınmak gerekir.
    • Mantığın çoğunu paketler olarak yazma eğilimindeyim. Örneğin HN’i yazıyor olsaydım users paketi ya da comments paketi gibi ayırırdım.
      Bu paketlerde hiç HTTP arayüzü yoktur, ama her birinin kendi main’i ve bir tür CLI arayüzü vardır. Bu dosya yorumundaki //go:build ignore işe yarar.
    • Sadece closure kullanıyorum.
      Handler’ı func HandleX(w http.ResponseWriter, req *http.Request) gibi tanımlamak yerine, func HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFunc biçiminde gerekli bağımlılıkları alıp içeride gerçek http.HandlerFunc döndürecek şekilde yazıyorum.
      Sonra giriş noktasında bir kez başlatıyorum.
    • Bu, NewServer’ın oluşturduğu nesnenin çok fazla iş yaptığı anlamına geliyor. Çok fazla veri tipi ve davranışın birbirine bağlanmış olma ihtimali yüksek.
      Basit bir örnek olarak constructor bağımlılığına bir logger eklediğinizde, nesne başlangıçtaki basit uygulamadan biraz daha fazla iş yapar. Bu başlı başına sorun değil; ancak basit olanın uygulamasını değiştirmeden loglama yapmanın bir yolunu bulamamak üzücü.
      Üst seviye fonksiyonlar, örneğin bir logger decorator’ı, bileşime imkân verir ama bunun da dezavantajları vardır. Yine de bu, ele alınabilir bir yapının bir biçimidir; hata değildir.
    • Uzun süre aynı hissi taşıdım ve artık isteğe bağlı ayar struct’ı kullanan bir yaklaşıma geçtim.
      Esas nokta, NewServer içinde isteğe bağlı ayar struct’ındaki değerleri doğruladıktan sonra bunları sunucu struct’ına kopyalamak. Böylece yalnızca daha az bağımlılığı mock’lamak yeterli olduğundan testler çok daha kolaylaşıyor.
      Birçok kişinin önerdiği gibi function options pattern’ını da çok denedim ama sonunda vazgeçtim. Biraz fazla zeki işi olduğu için okunması zor; ayrıca ayar struct’ı + doğrulayıp kopyalama kalıbına kıyasla daha fazla boilerplate içerdiğini hissettim.
      [0] https://news.ycombinator.com/item?id=39320170
  • Hangi dilde olursa olsun HTTP servisleri için şu düşüncenin daha yaygın kabul görmesini isterdim: Bir handler’ın bağımlılığa ihtiyacı varsa bunu doğrudan argüman olarak istemeli; sunucu struct’ına asılmış bir metot olarak durup test sırasında sürpriz bağımlılıklar yaratmamalı.
    HTTP servislerindeki handler’larda genellikle çok fazla iş mantığı olur ve bu mantığın çok sayıda bağımlılığı olması muhtemeldir. Tek bir handler’ın DB, cache, blob storage, endpoint’e özel yetki kontrolü, lisans denetleyicisi, kuyruk, özel logger, metrik istemcisi vb. kullandığını sıkça gördüm.
    Parametre sayısı 9’u aşabilir ve linter’lar ya da pratik kurallar genelde bunu engellemeye çalışır; fakat bağımlılıklar ortadan kaybolmaz, sadece server class/struct’ının içine saklanır ve metot imzası kısa olduğu için bağımlılık azmış gibi kendinizi kandırırsınız.
    Zamanla 20’ye çıksa bile tüm bağımlılıkların fonksiyon/metot imzasında görünür olduğu kodun daha iyi olduğunu düşünüyorum. Böylece kod karmaşıklığının büyüdüğünü kendinizden gizlememiş olursunuz.

    • Handler’ları her zaman route/isteği işleyen metoda sahip ayrı struct’lar olarak tutarım.
      Örneğin CreateUser struct’ının içine o işe gereken store, cache, logger, pub gibi bağımlılıkları koyar ve ServeHTTP’yi uygularım.
      main.go’da ya da bağımlılıkları kurduğunuz yerde her işi oluşturur, yalnızca gerekli bağımlılıkları geçirirsiniz. Bu sayede belirli bir işin/handler’ın yardımcı metotlarını o struct’ın private metotları olarak tutabilmek güzel oluyor.
      Ancak bir iş başka bir işe ihtiyaç duyarsa birbirlerine geçirmeye başlamak ya da ayrı bir paket/servis olarak çıkarmak gerekebilir; bu da zahmetli olabilir.
    • Mutlaka 9’dan fazla ayrı argüman olması gerekmez. Bazı dillerde handler’ın ihtiyaç duyduğu şeyleri içeren tek bir context/env nesnesi kullanılabilir.
      Örneğin handleHello({ db, cache, blobStore, authz }, req, res) gibi yazarsanız, iki handler tam olarak aynı context’i kullanıyorsa yeniden kullanabilirsiniz; çağrı noktasında handler’a özel context’i bildirmek de kolay olur.
  • Bu yazının büyük kısmına katılıyorum ve birkaç şey daha eklemek istiyorum.
    Uygulama context’iyle birlikte WaitGroup’u servis struct’ına geçirirseniz, interrupt context üzerinden uygulamanın kapanmasını tetikleyebilir ve ana goroutine gerçek kapanıştan önce WaitGroup’u bekleyebilir.
    Bir CLI programıysa stdout, stdin, stderr, args, env gibi şeyleri test etmek faydalıdır; ancak HTTP sunucusunda bunun daha az geçerli olduğunu düşünüyorum. run fonksiyonuna yapılandırılmış ayarları geçirerek testlerin daha odaklı olmasını sağlardım.
    Handler içinde sync.Once ile template parse etme yaklaşımına karşıyım. Handler’ın template parse etmemesi gerektiğini, bunun uygulama başlangıcında yapılması gerektiğini düşünüyorum. Template parse edilemiyorsa uygulama istek almaya hazır duruma gelmemeli ve sıfır olmayan bir çıkış koduyla sonlanmalı.

    • İlk nokta ilginç. Bunun context propagation ve sunucunun kapanmasını bekleme ile çözülen bir sorun olup olmadığını merak ediyorum.
  • Son zamanlarda ogen ile oynuyorum: https://github.com/ogen-go/ogen
    Bir OpenAPI tanımı yazınca yönlendirme, struct tanımları, JSON şema doğrulaması vb. işleri yapıyor. Bana düşen tek şey servisi uygulamak
    Sorgu dizesindeki tamsayı aralığı doğrulaması gibi şeyler çok sıkıcı ve elle yazınca yazım hatası yapmak da çok kolay
    Şimdilik sadece deneme aşamasındayım, kötü bir yanını görmedim

    • Bu yaklaşımın sorunu, OpenAPI’yi baştan elle yazma sürecinin aşırı sıkıcı olması
      Protobuf, capnproto gibi benzer bir IDL yazmak çok daha üretken hissettiriyor
    • OpenAPI spesifikasyonunu Go kodundan dışa aktarmayı tercih ediyorsanız danielgtaylor/huma[1] ve swaggest/rest[2] de iyi seçenekler
      [1] https://github.com/danielgtaylor/huma
      [2] https://github.com/swaggest/rest
    • Benzer şekilde oapigen ile başladım: github.com/deepmap/oapi-codegen
      Spesifikasyon yazmanın sıkıcı olacağını sanıyordum ama beklediğimden çok daha iyiydi; zaten spesifikasyona ihtiyaç olacağı için baştan yazmanın daha iyi olduğunu düşünüyorum
  • fx’in (https://github.com/uber-go/fx) bir uygulama tasarlamak için çok basit ama çok yönlü bir araç olduğunu düşünüyorum
    Yazıdaki tavsiyeler hâlâ faydalı, ama “Y ihtiyaç duyduğunda X’in başlatılmış olduğunu nasıl garanti ederim?” kısmını tamamen ortadan kaldırıyor. N*M problemi N problemine iniyor; her parçanın nasıl başlatılacağına odaklanmak yeterli oluyor, parçalar arasındaki başlatma senkronizasyonunu dert etmeye gerek kalmıyor
    Birçok dilde bağımlılık enjeksiyonu kütüphanesi epey kullandım ve kendim de uyguladım; fx’in sadeliği ve genelliği şimdiye kadar en çok hoşuma giden oldu

    • Bu tür bağımlılık enjeksiyonu frameworklerinden gerçekten nefret ediyorum
      İyi tasarlanmış bir sistemde bu sorun önemsiz olmalı. Bir şeyi kullanmak istediğinizde başlatılmış olmasını garanti etmek, onun constructor argümanı olarak verilmeye hazır olup olmadığı meselesidir
      stockService := NewStockService(), orderService := NewOrderService(), orderProcessor := NewOrderProcessor(stockService, orderService) gibi oluşturursunuz
      Başlatma “senkronizasyonu” gibi bir şeye ihtiyaç olmamalı; yanlış yaparsanız derlenmez. Döngüsel bağımlılık ekleseniz bile doğru sırayla bir araya getiremeyeceğiniz için açıkça ortaya çıkar
  • İlginç fikirleri olan harika bir yazı. signal.NotifyContext’i bilmediğime inanamıyorum
    Artık her projeye kopyala-yapıştır yapmadan sinyal işleme yöntemini hatırlayabileceğim

  • Buradaki yaklaşımı çok beğeniyorum, ama benim testlerim biraz farklı
    newTestServer() içinde bağımlılıkları fake’lerle doldurulmuş bir sunucu başlatıyorum; bir bağımlılık hatasını test etmek istersem ilgili özelliği hata döndüren bir fake ile değiştiriyorum
    Böylece hata yollarını, log kayıtlarını, metrik yayınlamayı, timeout’ları ve graceful shutdown’ı doğrulayabiliyorum
    Sunucu başladıktan sonra hangi porta bind edildiğini kontrol ediyorum. Varsayılan değer :0 olduğu için gerçekten atanmış portu beklemek gerekiyor
    “Birim” testlerini handler seviyesinde veya HTTP seviyesinde yapabiliyorum; middleware’in tamamından geçirerek ya da hiç geçirmeden, kodu kullanıcının göreceği biçimde yeterince test edebiliyorum. N adet instance başlatıp paralel test etmek de mümkün

  • Go kullanmıyorum ama bu kalıplar hoşuma gidiyor. Test edilebilir kod için oldukça evrensel biçimde uygulanabilir görünüyor
    Bağımlılıkları örtük/statik/test edilemez şekilde ele alan, özellikle de Python hızlı başlangıç kılavuzlarını bir daha görmek istemiyorum