13 Yıl Sonra Go ile HTTP Servisleri Nasıl Yazılır
(grafana.com)- 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
runfonksiyonu kullanıldığında bakım ve doğrulama kolaylaşır - Handler’ları sunucu struct’ı metotları yerine gerekli değerleri closure olarak alan
http.Handlerdöndüren fonksiyonlar olarak yazın; ortak middleware’leri sunucu oluşturma ve route kaydı aşamasında birleştirin func main()’i ince tutuprun()içinecontext.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.Onceile gecikmeli başlatma, tekrar eden kodu azaltırken Go’nun standartnet/httpakışı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
/healthzya da/readyzile kontrol eder
Sunucu oluşturma ve servis giriş noktası
NewServerconstructor’ı, servisin temelhttp.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.Handlerdöndürür; karmaşık durumlarda özel bir tip kullanılabilir - Kendi muxer’ını oluşturduktan sonra
routes.goiçindeki route kayıt fonksiyonuna geçirir
- Tüm endpoint’ler için ortak gerekli HTTP işlemleri
NewServeriç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
nilgeç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 nedeniyleaddRoutesiçinde de benzer bir argüman listesi oluşabilir- Go’nun tip denetimi eksik ya da sırası hatalı argümanları yakalar
addRoutesmümkün olduğunca basit ve düz tutulur- Hata verebilecek işlemler önce
runfonksiyonunda ele alınır - Handler kayıt aşamasında
mux.Handle,mux.HandleFunc,http.NotFoundHandlergibi routing işlerine odaklanılır - Handler’ın kendisinin hata döndürmesi gereken bir tasarım varsa
addRoutesda hata döndürebilir
- Hata verebilecek işlemler önce
main sadece run’ı çağırsın
func main(),run()’ı çağıran; hata varsastderr’e yazıp anormal şekilde çıkan ince bir fonksiyon olarak tutulurrun,context.Context, argümanlar, giriş/çıkış ve ortam erişim fonksiyonu gibi işletim sistemi temel bileşenlerini argüman olarak alırrunhata 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ıros.Stdin: giriş okumak içinos.Stdout: çıktı yazmak içinos.Stderr: hata logları yazmak içinos.Getenv: ortam değişkenlerini okumak içinos.Getwd: geçerli çalışma dizinini almak için
signal.NotifyContext,runiçinde ayarlanırCtrl+Cgibi kapatma sinyali geldiğinde context iptal edilirrunnildöndürürse normal çıkış yapılır- Hata döndürürse
mainhatayı yazdırır ve sıfır olmayan kodla çıkar
- Global durumdan kaçınıldığında daha fazla testte
t.Parallel()kullanılabilirrunbirden fazla kez çağrılsa da her çalıştırma birbirini etkilemez- Flag’ler global
flagyerineruniçindekiflags.NewFlagSetile işlenir - Ortam değişkenleri gerçek ortamı değiştirmek yerine
getenv func(string) stringenjekte 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 dactx.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.Secondtimeout konur - Kapatma sırasında hata olursa
stderr’e kaydedilir
- Örnekte ayrı bir goroutine içinde
- Testlerde sunucunun gerçekten hazır olup olmadığını kontrol etmek için
/healthzya da/readyzendpoint’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 OKgelene kadar istek yapar - Context iptali ya da timeout’a ulaşılırsa hata döndürür
- Örnek döngü istekler arasında
250msbekler
Handler oluşturma biçimi
- Handler fonksiyonları
http.Handlerya dahttp.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
- Örnek:
- 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/decodehelper’ları bulundurulur- Örnek, JSON
Content-Type’ını ayarlar, durum kodunu yazar ve ardındanjson.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 decodedönüş tipine bağlı olduğundandecode[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
- Örnek, JSON
- Doğrulama için tek metotlu bir interface kullanılır
Validatorinterface’iValid(ctx context.Context) map[string]stringbiç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],Ttipinin mutlakaValidatorimplement etmesini zorunlu kılar nilmap üzerindelen(problems)çağırmak 0 döndürür, bu yüzden panic oluşmaz
Middleware adaptör paterni
- Middleware bir
http.Handleralıp yeni birhttp.Handlerdö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ğilseHTTP 404 Not Founddöndürür ve asıl handler’ı çağırmaz
- Middleware uygulama yeri genellikle
routes.goolur- 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)birfunc(http.Handler) http.Handlerdöndürür- Route kayıt kodunda
middleware(handleSomething(...))gibi kısa kullanılır - Ayrı bir
type middleware func(h http.Handler) http.Handlertanı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
/greetendpoint’iPerson’ın tamamına değil yalnızcaNamealanına ihtiyaç duyuyorsa test giriş struct’ında sadeceNamebulunur - Testi okuyan kişi bu endpoint’in hangi alanla ilgilendiğini hemen anlar
- Örneğin
sync.Once ile başlatmayı geciktirmek
- Handler hazırlanırken maliyeti yüksek işler
sync.Onceile 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.Dodışı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
runfonksiyonu 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.NewRecordervehttp.NewRequestile 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.goda 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.WithCancelile cancel fonksiyonu oluşturulur vet.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ğrudandeferkullanmaya 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
Hacker News yorumları
Validmetodu 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
Usernametipi veNewUsername(username string) (Username, error)kurucusu tanımlarsanız, birUsernamenesnesinin 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...
stringolarak geçirmek yerine parse edip tipe bağlamak gerekirBu 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/
Usernamebir struct’ın içinde yer alıyorsa ve değeri ayarlamayı unutursanız, kısıtı ihlal edebilecek bir zero value girerTü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
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 birdataclassiçinde bulunması oldukça kullanışlı bir tasarım deseni oluyorOptionfonksiyonlarıyla oluşturulabilir kılmakİçteki
optionsyalnızca oluşturma sırasında değiştirilir, dışarıya ise sadeceConfigve erişim metotları açılır. Örneğinconfig.New(config.Name("Emanon"))gibi oluşturupcfg.Name()ile okuyabilirsinizConfigstruct’ı oluşturup,configs.Config’i de her paketinConfig’lerini bir araya getiren bir yapı yapmak eğilimindeyimBu 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
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_USEAPI’sini kullanmak gerekiyordu; yine de onunla nesneyi değiştirip kesintiye yol açmıştımMat 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çinnilgeç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.
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.
userspaketi ya dacommentspaketi 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 ignoreişe yarar.Handler’ı
func HandleX(w http.ResponseWriter, req *http.Request)gibi tanımlamak yerine,func HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFuncbiçiminde gerekli bağımlılıkları alıp içeride gerçekhttp.HandlerFuncdöndürecek şekilde yazıyorum.Sonra giriş noktasında bir kez başlatıyorum.
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.
Esas nokta,
NewServeriç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
serverclass/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.
Örneğin
CreateUserstruct’ının içine o işe gerekenstore,cache,logger,pubgibi bağımlılıkları koyar veServeHTTP’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.
Ö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.
runfonksiyonuna yapılandırılmış ayarları geçirerek testlerin daha odaklı olmasını sağlardım.Handler içinde
sync.Onceile 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ı.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
Protobuf, capnproto gibi benzer bir IDL yazmak çok daha üretken hissettiriyor
[1] https://github.com/danielgtaylor/huma
[2] https://github.com/swaggest/rest
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
İ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şturursunuzBaş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ıyorumArtı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ştiriyorumBö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
:0olduğ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