2 पॉइंट द्वारा GN⁺ 2024-02-10 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • लंबे समय तक चलने वाली Go HTTP सेवाओं को स्पष्ट dependency passing, एक जगह इकट्ठे routes, और testable run function के साथ बनाया जाए तो maintenance और verification आसान हो जाते हैं
  • handlers को server struct methods के बजाय जरूरी values को closure के रूप में लेने वाले http.Handler लौटाने वाले functions के रूप में बनाएं, और common middleware को server creation और route registration चरणों में compose करें
  • func main() को पतला रखें और run() में context.Context, arguments, environment access, standard input/output inject करने से shutdown handling और test control सरल हो जाते हैं
  • request/response encoding, validation, middleware adapters, sync.Once lazy initialization repetitive code को घटाते हैं, फिर भी Go standard net/http flow बनाए रखते हैं
  • tests में individual handlers के बजाय वास्तविक API calls के करीब end-to-end approach को प्राथमिकता दी जाती है, और हर test अपना server चलाकर /healthz या /readyz से readiness check करता है

Server creation और service entry point

  • NewServer constructor को service का core http.Handler बनाने वाला function रखें
    • आम तौर पर हर service के लिए एक होता है, और internal routes requests को अलग-अलग handlers में dispatch करते हैं
    • logger, config, storage, external clients जैसी सभी dependencies arguments के रूप में लें
    • संभव हो तो http.Handler लौटाएं, और complex cases में dedicated type इस्तेमाल किया जा सकता है
    • अपना muxer configure करने के बाद उसे routes.go के route registration function को दें
  • सभी endpoints के लिए common HTTP processing को NewServer में group करें
    • CORS
    • authentication middleware
    • logging
    • trace ID middleware
  • dependency arguments की list लंबी हो जाए तब भी function arguments के रूप में explicit करने का तरीका पसंद करें
    • struct fields छूट जाने पर compiler रोक नहीं सकता, लेकिन function arguments में जरूरी value न देने पर call ही नहीं हो सकता
    • लंबी argument list को vertically format करने पर पढ़ना आसान होता है
    • किसी खास test में इस्तेमाल न होने वाली dependency को nil देकर यह signal मानें कि उसका उपयोग नहीं होगा

API surface को routes.go में इकट्ठा करना

  • routes.go को service के सभी routes एक जगह देखने वाली file के रूप में रखें
    • हर project में API surface को scan करने के लिए single location मिलता है
    • NewServer की बड़ी dependency list के कारण addRoutes में भी मिलती-जुलती argument list हो सकती है
    • Go type checking missing या गलत order वाले arguments पकड़ लेती है
  • addRoutes को संभव हो तो सरल और flat रखें
    • error पैदा कर सकने वाले काम पहले run function में handle करें
    • handler registration चरण में mux.Handle, mux.HandleFunc, http.NotFoundHandler जैसे routing पर focus करें
    • अगर design में handler को खुद error return करना है, तो addRoutes भी error return कर सकता है

main से सिर्फ run call करना

  • func main() को run() call करने वाला, error होने पर stderr में लिखकर abnormal exit करने वाला पतला function रखें
    • run operating system के basic elements जैसे context.Context, arguments, input/output, environment access functions को arguments के रूप में लेता है
    • run error return करता है, इसलिए normal Go code की तरह error handling संभव है
  • run को दिए जा सकने वाले values के examples ये हैं
    • os.Args: program execution arguments और flag parsing के लिए
    • os.Stdin: input पढ़ने के लिए
    • os.Stdout: output लिखने के लिए
    • os.Stderr: error logs लिखने के लिए
    • os.Getenv: environment variables पढ़ने के लिए
    • os.Getwd: current working directory पाने के लिए
  • signal.NotifyContext को run के अंदर set करें
    • Ctrl+C जैसे shutdown signal आने पर context cancel हो जाता है
    • run अगर nil return करे तो normal exit होता है
    • error return करने पर main error print करता है और non-zero code से exit करता है
  • global state से बचने पर ज्यादा tests में t.Parallel() इस्तेमाल किया जा सकता है
    • run को कई बार call करने पर भी हर execution एक-दूसरे में interfere नहीं करता
    • flags को global flag के बजाय run के अंदर flags.NewFlagSet से handle करें
    • environment variables को actual environment बदलने के बजाय getenv func(string) string inject करके control करें
    • यह तरीका t.SetEnv के विपरीत parallel tests जारी रखने देता है

Shutdown और readiness handling

  • context को service की सभी layers तक pass करना चाहिए
    • shutdown signal आने पर context cancel हो जाता है
    • लंबे या repeated tasks ctx.Err() या ctx.Done() check करके रुकते हैं
    • दूसरे goroutines start किए हों तब भी context से decide करें कि कब रुकना है
  • HTTP server shutdown पर Shutdown call करके gracefully रुकता है
    • example में अलग goroutine में ctx.Done() का wait किया जाता है
    • shutdown context में 10 * time.Second timeout रखा जाता है
    • shutdown के दौरान error हो तो stderr में record करें
  • tests में server सच में ready है या नहीं check करने के लिए /healthz या /readyz endpoints रखें
    • अलग channel से readiness signal बनाया जा सकता है, लेकिन actual HTTP request से check करने का तरीका पसंद है
    • readiness check loop 200 OK आने तक requests करता है
    • context cancellation या timeout तक पहुंचने पर error return करता है
    • example loop requests के बीच 250ms sleep करता है

Handler composition style

  • handler functions http.Handler या http.HandlerFunc को directly implement करने के बजाय उन्हें return करें
    • जैसे: func handleSomething(logger *Logger) http.Handler
    • हर handler के लिए closure environment बनाया जा सकता है
    • initialized values को request processing के दौरान इस्तेमाल किया जा सकता है
  • shared data को केवल read-only रखना सुरक्षित है
    • अगर handler values modify करता है, तो mutex जैसी protection चाहिए
    • program state को closure में store करने का तरीका आम तौर पर recommend नहीं किया जाता
  • cloud environments में यह assume करना मुश्किल है कि instance लंबे समय तक रहेगा
    • server resource बचाने के लिए down हो सकता है, या किसी और वजह से crash हो सकता है
    • कई instances साथ-साथ चल सकते हैं और requests unpredictable तरीके से distribute हो सकते हैं
    • real project की persistent state database या अलग storage API में रखना बेहतर है

Request/response encoding और validation

  • हर service को request body decoding और response body encoding चाहिए, इसलिए encode / decode helpers रखें
    • example JSON Content-Type set करता है, status code लिखता है और फिर json.NewEncoder(w).Encode(v) call करता है
    • decoding json.NewDecoder(r.Body).Decode(&v) को wrap करके error में context जोड़ती है
    • generics इस्तेमाल करने पर encode(w, r, http.StatusOK, obj) जैसे type inference संभव है
    • decode return type है, इसलिए decode[CreateSomethingRequest](https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/r) की तरह expected type specify करना होगा
  • validation के लिए single-method interface इस्तेमाल करें
    • Validator interface का shape Valid(ctx context.Context) map[string]string है
    • कोई problem न हो तो zero length का map return होता है
    • problem वाले fields में field name को key और human-readable description को value रखें
  • validation target तेज field checks के लिए उपयुक्त है
    • required fields खाली नहीं हैं या नहीं
    • email जैसी specific string format सही है या नहीं
    • number allowed range के अंदर है या नहीं
  • database query जैसे ज्यादा complex checks अलग जगह handle करें
    • ऐसे checks quick validation function के अंदर छिपाने के लिए बहुत important होते हैं
    • generic version decodeValid[T Validator] type T को अनिवार्य रूप से Validator implement करने के लिए enforce करता है
    • nil map पर len(problems) call करने पर भी 0 आता है, इसलिए panic नहीं होता

Middleware adapter pattern

  • middleware http.Handler लेता है और नया http.Handler return करता है
    • original handler call से पहले/बाद code run कर सकता है
    • condition के आधार पर original handler को call न भी कर सकता है
    • example का adminOnly admin न होने पर HTTP 404 Not Found return करता है और original handler call नहीं करता
  • middleware apply करने की जगह आम तौर पर routes.go रखें
    • endpoints list देखकर ही पता चलता है कि किस route पर कौन सा middleware लगा है
    • middleware list लंबी हो जाए तो पढ़ने में आसान बनाने के लिए कई lines में split करें
  • ज्यादा dependencies वाले middleware को middleware return करने वाले function में wrap करें
    • newMiddleware(logger, db, slackClient, rroll) func(http.Handler) http.Handler return करता है
    • route registration code में middleware(handleSomething(...)) जैसे concise तरीके से इस्तेमाल करें
    • अलग type middleware func(h http.Handler) http.Handler define किया जा सकता है, लेकिन return type direct लिखने से code reading ज्यादा clear होती है

Request/response types का scope छोटा करना

  • केवल किसी specific endpoint में इस्तेमाल होने वाले request/response types को handler function के अंदर define किया जा सकता है
    • global namespace साफ रहता है
    • दूसरे handlers को ऐसे types पर depend करने से रोकता है जिनकी stability guarantee नहीं है
  • test code में वही type चाहिए तो friction हो सकता है
    • ऐसे मामले में type को बाहर निकालना भी valid है
    • request/response type handler के अंदर हो तो tests में नया anonymous struct या local type declare किया जा सकता है
  • tests के local types intent दिखाते हैं
    • उदाहरण के लिए /greet endpoint को पूरे Person की नहीं, सिर्फ Name field की जरूरत है, तो test input struct में केवल Name रखें
    • test पढ़ने वाला तुरंत समझ सकता है कि endpoint किन fields में interested है

sync.Once से lazy initialization

  • handler तैयार करते समय costly work को sync.Once से पहली request तक defer करें
    • application startup time कम होता है
    • handler call न हो तो costly work execute नहीं होता
  • example template file parsing को पहली request पर सिर्फ एक बार करता है
    • sync.Once guarantee करता है कि code केवल एक बार run होगा
    • concurrently आने वाली दूसरी requests initialization खत्म होने तक wait करती हैं
    • error check init.Do के बाहर करें ताकि error लगातार surface होता रहे
  • यह तरीका initialization time को startup से runtime के first endpoint access पर shift करता है
    • Google App Engine ज्यादा इस्तेमाल करने वाले environment में यह तरीका fit हो सकता है
    • deployment environment के अनुसार तय करें कि sync.Once कहां और कब इस्तेमाल करना है

Testing strategy

  • यह structure testability को important goal मानता है
    • run function test code में program को directly run करने देता है
    • tests का criteria है कि program behavior समझना आसान है या नहीं, changes पर टूटने की चिंता कम होती है या नहीं, और pass होने के बाद production deploy पर confidence मिलता है या नहीं
  • केवल handlers को independently test भी किया जा सकता है
    • handler creation function call करके जरूरी dependencies दें
    • httptest.NewRecorder और http.NewRequest से request और response construct करें
    • status code, response body, headers verify करें
    • यह तरीका authentication जैसे middleware को skip करके सीधे handler code में जाता है
  • ज्यादा पसंदीदा तरीका end-to-end tests के करीब है
    • run call करके actual execution के करीब program start करें
    • argument parsing, dependency wiring, database migrations, server start तक शामिल करें
    • test API call करता है तो सभी layers और routes.go भी साथ में verify होते हैं
    • actual database के साथ interact कर सकता है
  • यह approach repetitive tests घटाने में मदद करता है
    • सभी layers को अलग-अलग test करने पर वही content थोड़े अलग तरीके से कई बार verify हो सकता है
    • end-to-end tests users और system interaction को describe करने वाला central test set देते हैं
    • TDD आदि से पहले से बने unit tests ठीक हों तो रखें, लेकिन end-to-end tests जैसी ही चीज repeat करें तो हटाए जा सकते हैं
  • हर test अपना program instance run कर सकता है
    • हर test अलग arguments, flags, standard input/output, environment variables देता है
    • context.WithCancel से cancel function बनाकर t.Cleanup(cancel) में register करें
    • test खत्म होने पर context cancel होता है और program gracefully shutdown होता है
    • Go 1.14 का t.Cleanup direct defer के विकल्प के रूप में इस्तेमाल होता है

Practical application scope और organization context

  • simple API बनाते समय यह pattern readable और scalable code का लक्ष्य रखता है
    • pattern copy करके extend करना आसान है
    • नए लोगों के लिए काम करना आसान है
    • changes पर anxiety कम होती है
    • magic behavior के बिना explicitly configure होता है
  • code generation tools इस्तेमाल करने पर भी यह तरीका बना रह सकता है
    • उदाहरण के लिए template-based boilerplate generation के लिए Oto package इस्तेमाल किया जा सकता है
  • बड़े projects या बड़े organizations में existing technology choices decisions बदल सकते हैं
    • Grafana Labs जैसे organizations में specific tools और abstractions पहले से widely used हो सकते हैं
    • gRPC ऐसा ही example है
    • established patterns और experience होने पर उस flow का पालन करना practical choice बन जाता है
  • Grafana IRM product suite का context भी शामिल है
    • Grafana IRM Grafana Labs में बनाया जा रहा product suite है
    • Grafana Alerting metrics allowed range से बाहर जाने पर notifications भेजता है
    • Grafana OnCall schedules और escalation rules के जरिए सही व्यक्ति से contact करने की प्रक्रिया automate करता है
    • Grafana Incident Zoom rooms, dedicated Slack channels, event timeline बनाता है और incident response में मदद करता है
    • Slack channel में robot face emoji reaction लगे items timeline में add होते हैं

1 टिप्पणियां

 
GN⁺ 2024-02-10
Hacker News टिप्पणियाँ
  • मैंने Valid मेथड की तरह अलग validator रखने वाला तरीका भी इस्तेमाल किया है, लेकिन Lexi Lambda का “Parse, Don’t Validate” [0] पढ़ने के बाद से मुझे लगता है कि Go के type checker का उपयोग करने पर गलतियाँ कहीं कम होती हैं
    उदाहरण के लिए, अगर आप यह सुनिश्चित करना चाहते हैं कि user कभी भी angle brackets वाला अवैध username सेट न कर सके, तो validator वाले तरीके में आपको untrusted input से username आने वाले हर code path पर validator कॉल करना पड़ेगा
    इसके बजाय अगर Username type और NewUsername(username string) (Username, error) constructor हो, तो सिर्फ Username object के मौजूद होने से ही यह गारंटी मिल जाती है कि वह validation पास कर चुका है
    [0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

    • type system का सही इस्तेमाल करने से code बेहतर होता है, यह बात फिर से चौंकाती है। हर चीज़ को string की तरह पास करने के बजाय, उसे parse करके type देना चाहिए
    • यह अच्छा design pattern है, लेकिन बहुत जल्दी validation करने से सावधान रहना चाहिए
      यह pattern आपको validation जल्दी भी करने देता है और देर से भी, लेकिन यह नहीं बताता कि उसे कब करना चाहिए। अक्सर बड़ा object parse/validate करने की प्रक्रिया के हिस्से के रूप में यह करना बेहतर होता है
      UI context में unvalidated data को संभालने के विचार के लिए Steven Witten का “I is for Intent” [1] देखना उपयोगी हो सकता है
      [1] https://acko.net/blog/i-is-for-intent/
    • अवधारणात्मक रूप से यह पुरानी private constructor और factory method तकनीक जैसा ही है
    • संबंधित लिंक: Parse, don't validate (2019) - https://news.ycombinator.com/item?id=35053118 - मार्च 2023, Parse, Don't Validate (2019) - https://news.ycombinator.com/item?id=27639890 - जून 2021, Parse, Don’t Validate - https://news.ycombinator.com/item?id=21476261 - नवंबर 2019, Parse, Don't Validate - https://news.ycombinator.com/item?id=21471753 - नवंबर 2019
    • Go में इस pattern को लागू करना मुश्किल था। अगर Username किसी struct के अंदर शामिल हो और आप उसका मान सेट करना भूल जाएँ, तो constraints तोड़ सकने वाला zero value आ जाता है
  • पूरे system configuration को दर्शाने वाला Config object लेकर उसे जगह-जगह mutable रूप में पास करना मुझे सबसे नापसंद patterns में से एक लगता है
    ऐसा करने पर हर चीज़ config object के ज़रिए एक-दूसरे से जुड़ जाती है। कुछ systems में किसी ने पास किया गया config object फिर से लिख दिया, और सही तरह से चलाने के लिए हर हिस्से को एक खास क्रम में configure करना पड़ता था
    दूसरे मामलों में किसी subsystem ने बाद में पढ़े जाने वाले data को config object में लिख दिया, जिससे system के कुछ हिस्सों को disable नहीं किया जा सका
    “configuration एक बड़ा mutable value है” वाला pattern सिर्फ Go में नहीं, दूसरी भाषाओं में भी काफ़ी परेशान करने वाला है

    • मुद्दा सामान्य configuration data object नहीं, बल्कि mutable configuration object है
      Python projects में मैं immutable config dataclass का काफ़ी उपयोग करता हूँ और उसे कई modules में पास करता हूँ। जब कई functions कई values पर निर्भर हों, तो हर value को function argument के रूप में पास करने और types अलग से define करने के बजाय, एक ही dataclass में सारे variables और type definitions होने से यह काफ़ी सुविधाजनक design pattern बन जाता है
    • इसे रोकने का मेरा पसंदीदा तरीका है configuration को सचमुच immutable बनाना, लेकिन Option functions से उसे composable रखना
      अंदरूनी options को सिर्फ construction के दौरान बदला जाए, और बाहर सिर्फ Config और accessors expose किए जाएँ। उदाहरण के लिए config.New(config.Name("Emanon")) की तरह बनाया जा सकता है और cfg.Name() से पढ़ा जा सकता है
    • मैं हर package के लिए एक Config struct बनाता हूँ, और configs.Config को उन package-स्तरीय Config का संग्रह मानता हूँ
      यह शायद Go best practice न हो, लेकिन शुरुआत में पूरे system configuration को एक entity की तरह बनाया जा सकता है, और हर package को सिर्फ उसकी न्यूनतम आवश्यक dependencies दी जा सकती हैं, इसलिए यह अच्छा लगता है
      testing के समय भी सिर्फ एक package test करने के लिए पूरा configuration fake बनाने की ज़रूरत नहीं पड़ती, इसलिए थोड़ी आसानी हो जाती है
    • सहमत। एक बार top-level config object बदलने पर मैंने गंभीर outage करा दिया था
      उसे कभी modify नहीं करना चाहिए। यह पता नहीं चलता कि कहाँ और कैसे इस्तेमाल हो रहा है, इसलिए इसमें मानव-घंटों की बर्बादी बहुत महँगी पड़ती है; अगर बदलाव चाहिए, तो मूल से derived value बनाना बेहतर है
      मज़ेदार बात यह है कि design के हिसाब से config object कुछ हद तक immutable था, और उसे modify करने के लिए WARNING_DO_NOT_USE API इस्तेमाल करनी पड़ती थी, फिर भी उसी से object बदलकर outage कर दिया गया
    • मुझे यह उचित आलोचना लगती है। जानना चाहूँगा कि आपके हिसाब से इससे अधिक ergonomic pattern क्या है
  • मुझे Mat Ryer का काम सच में बहुत पसंद है, और इस लेख के 2018 वाले संस्करण में आए ज़्यादातर विचारों को मैंने उसके बाद से हर Go प्रोजेक्ट में लागू किया है
    लेकिन NewServer का सभी dependencies को arguments के रूप में लेने वाला एक बड़ा constructor होना, और tests में जिन dependencies की ज़रूरत नहीं है उनके लिए nil पास करना — यह बात मुझे हमेशा असहज लगती रही
    नतीजा यह होता है कि code का बड़ा हिस्सा बहुत सारा अनावश्यक shared state रखने लगता है। वास्तव में बहुत से HTTP handlers को बस यह जांचना होता है कि request करने वाला user resource तक पहुँच सकता है या नहीं, और data store का सिर्फ एक function call करना होता है, लेकिन वे parent server के पास मौजूद सभी objects और पूरे data store तक पहुँच रखने वाले एक विशाल ढांचे का हिस्सा बन जाते हैं
    सिर्फ दो methods को mock करके test करना हो तब भी simple test लिखना मुश्किल हो जाता है, और Mat Ryer का pattern अब तक देखे गए patterns में सबसे अच्छा है, लेकिन फिर भी लगता है कि इससे बेहतर हल होना चाहिए

    • जिस repository पर मैं काम करता हूँ उसमें मैं ऐसे high fan-in coupling points के प्रति धीरे-धीरे अधिक संवेदनशील हो गया हूँ, और खासकर Bazel की दुनिया में जितना गहराई तक जाते हैं, dependency management और physical design का लिखे जाने वाले code पर असर उतना बढ़ता है
      जहाँ संभव हो, plugins अच्छे code boundary strategy होते हैं। plugin architecture मूल रूप से opt-out नहीं बल्कि opt-in होता है, इसलिए वह हर संभावना को किसी एक code blob पर थोपता नहीं है
      मैं इस तरह के software को “à la carte” कहता हूँ। सामान्य तौर पर “कुछ भी करने के लिए सब कुछ करने” वाली स्थिति से बचना चाहिए
    • मैं ज़्यादातर logic को packages के रूप में लिखता हूँ। उदाहरण के लिए अगर मैं HN बनाता, तो users package या comments package जैसा रखता
      इन packages का कोई HTTP interface नहीं होता, लेकिन हर एक का अपना main और किसी तरह का CLI interface होता है। उस file comment में //go:build ignore उपयोगी रहता है
    • मैं बस closures का इस्तेमाल करता हूँ
      func HandleX(w http.ResponseWriter, req *http.Request) की तरह handler define करने के बजाय, func HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFunc के रूप में ज़रूरी dependencies लेता हूँ और अंदर से असली http.HandlerFunc return करता हूँ
      और फिर entry point पर एक बार initialization करता हूँ
    • इसका मतलब है कि NewServer से बना object बहुत ज़्यादा काम कर रहा है। संभव है कि बहुत सारे data types और behaviors आपस में tightly coupled हों
      एक simple उदाहरण के लिए, अगर constructor dependency में logger जोड़ते हैं, तो object शुरुआती simple implementation की तुलना में थोड़ा ज़्यादा काम करने लगता है। अपने-आप में यह ठीक है, लेकिन अगर simple चीज़ की implementation बदले बिना logging करने का तरीका न मिले, तो वह खटकता है
      higher-order functions, जैसे logger decorators, composition संभव बनाते हैं, लेकिन उनके भी नुकसान हैं। फिर भी यह संभाली जा सकने वाली एक संरचना है, कोई गलती नहीं
    • मुझे भी लंबे समय तक यही महसूस हुआ, और अब मैं optional config struct इस्तेमाल करने वाले approach पर आ गया हूँ
      मुख्य बात यह है कि NewServer के अंदर optional config struct की values को validate किया जाए और फिर server struct में copy किया जाए। इससे कम dependencies को mock करना पड़ता है और testing बहुत आसान हो जाती है
      functional options pattern भी, जैसा बहुत लोग सुझाते हैं, मैंने काफी आज़माया लेकिन अंत में छोड़ दिया। वह कुछ ज़्यादा ही clever लगता है, पढ़ना मुश्किल हो जाता है, और मुझे लगता है कि config struct + validate-then-copy pattern की तुलना में उसमें boilerplate भी ज़्यादा है
      [0] https://news.ycombinator.com/item?id=39320170
  • काश किसी भी language की HTTP services में यह विचार और व्यापक रूप से स्वीकार किया जाए: अगर handler को dependencies चाहिएँ, तो उन्हें सीधे arguments के रूप में माँगना चाहिए; उन्हें server struct से चिपकी method के रूप में नहीं रखना चाहिए ताकि testing के समय hidden dependencies निकल आएँ
    HTTP service के handlers में आम तौर पर काफी business logic होता है, और उस logic की बहुत सारी dependencies हो सकती हैं। वास्तव में अक्सर एक single handler को DB, cache, blob storage, endpoint-specific authorization checks, license checker, queue, special logger, metrics client आदि का उपयोग करते देखा जाता है
    parameters 9 से भी ज़्यादा हो सकते हैं, और linters या rules of thumb आम तौर पर इसे रोकने की कोशिश करते हैं, लेकिन dependencies गायब नहीं होतीं — उन्हें बस server class/struct में छिपा दिया जाता है, और method signature छोटा होने से ऐसा दिखाया जाता है मानो dependencies कम हों
    समय के साथ मुझे लगता है कि अगर 20 dependencies भी हों, तब भी वह code बेहतर है जिसमें सभी dependencies function/method signature में साफ़ दिखाई दें। इससे code complexity बढ़ने की बात छिपाई नहीं जाती

    • मैं handlers को हमेशा route/request को संभालने वाली method रखने वाले अलग-अलग structs के रूप में रखता हूँ
      उदाहरण के लिए CreateUser struct के अंदर सिर्फ उसी काम के लिए ज़रूरी dependencies जैसे store, cache, logger, pub रखे जाते हैं और ServeHTTP implement किया जाता है
      main.go या जहाँ dependencies setup की जाती हैं, वहाँ हर action बनाया जाता है और केवल ज़रूरी dependencies पास की जाती हैं। इससे किसी खास action/handler की helper methods को उसी struct की private methods के रूप में रखना अच्छा लगता है
      लेकिन अगर कोई action किसी दूसरे action को ज़रूरी मानने लगे, तो उन्हें एक-दूसरे को pass करना शुरू करना पड़ता है या फिर अलग package/service में निकालना पड़ता है, जो झंझट बन सकता है
    • ज़रूरी नहीं कि 9 से ज़्यादा अलग-अलग arguments ही हों। कुछ languages में इसे एक single context/env object के रूप में रखा जा सकता है, जिसमें handler के लिए ज़रूरी चीज़ें ही हों
      उदाहरण के लिए handleHello({ db, cache, blobStore, authz }, req, res) की तरह लिखें, तो अगर दो handlers बिल्कुल वही context इस्तेमाल करते हैं तो उसे reuse किया जा सकता है, और call site पर handler-specific context declare करना भी आसान होता है
  • मैं इस लेख की बहुत-सी बातों से सहमत हूँ, और कुछ और जोड़ना चाहता हूँ
    app context के साथ WaitGroup को service struct में पास करने पर, interrupt context के ज़रिए app shutdown trigger कर सकता है और main goroutine वास्तविक shutdown से पहले WaitGroup का इंतज़ार कर सकती है
    अगर यह CLI program हो, तो stdout, stdin, stderr, args, env आदि को test करना उपयोगी है, लेकिन HTTP server में यह उतना उपयोगी नहीं लगता। run function को structured config देना बेहतर होगा ताकि tests ज़्यादा focused रहें
    handlers में sync.Once से templates parse करने के तरीके से मैं सहमत नहीं हूँ। मेरा मानना है कि handler को template parsing नहीं करनी चाहिए; यह app startup के समय होनी चाहिए। अगर templates parse नहीं हो सकते, तो app को requests लेने के लिए ready state में नहीं आना चाहिए और उसे non-zero exit code के साथ समाप्त हो जाना चाहिए

    • पहला बिंदु दिलचस्प है। क्या यह context propagation और server shutdown का इंतज़ार करके हल हो जाने वाली समस्या नहीं है?
  • हाल में ogen के साथ प्रयोग कर रहा हूँ: https://github.com/ogen-go/ogen
    OpenAPI definition लिखने पर यह routing, struct definition, JSON schema validation आदि संभाल लेता है। मुझे बस service implement करनी होती है
    query string integer range validation जैसी चीज़ें बहुत उबाऊ होती हैं, और खुद लिखो तो typo करना भी बहुत आसान है
    अभी बस इसे आज़माने के चरण में हूँ, इसलिए अभी तक इसकी कोई खराबी नहीं मिली है

    • इस approach की समस्या यह है कि OpenAPI को शुरू से हाथ से लिखने की प्रक्रिया बेहद उबाऊ है
      Protobuf, capnproto जैसी मिलती-जुलती IDL लिखना कहीं ज़्यादा productive लगता है
    • अगर आप Go code से OpenAPI spec export करना पसंद करते हैं, तो danielgtaylor/huma[1] और swaggest/rest[2] भी अच्छे हैं
      [1] https://github.com/danielgtaylor/huma
      [2] https://github.com/swaggest/rest
    • इसी तरह मैंने oapigen से भी शुरुआत की थी: github.com/deepmap/oapi-codegen
      लगा था कि spec लिखना उबाऊ होगा, लेकिन यह उम्मीद से कहीं बेहतर निकला, और वैसे भी spec चाहिए ही होती है, इसलिए मुझे लगता है कि उसे पहले से लिख लेना बेहतर है
  • fx(https://github.com/uber-go/fx) application design के लिए बेहद simple लेकिन versatile tool लगता है
    लेख की सलाह अब भी उपयोगी है, लेकिन यह “जब Y को ज़रूरत हो तब X initialized होने की गारंटी कैसे दें” वाले हिस्से को पूरी तरह हटा देता है। N*M समस्या घटकर N समस्या रह जाती है, इसलिए आपको बस यह सोचना होता है कि हर piece को कैसे initialize करना है, pieces के बीच initialization synchronization की चिंता नहीं करनी पड़ती
    मैंने कई भाषाओं में dependency injection libraries काफ़ी इस्तेमाल की हैं और खुद भी implement की हैं, लेकिन fx की simplicity और generality अब तक सबसे ज़्यादा पसंद आई है

    • मुझे ऐसे dependency injection framework सच में पसंद नहीं हैं
      अच्छे design वाले system में यह समस्या मामूली होनी चाहिए। जब आप कुछ इस्तेमाल करना चाहें, तब उसके initialized होने की गारंटी देना असल में इस बात का सवाल है कि क्या वह constructor argument के रूप में pass करने के लिए तैयार है
      stockService := NewStockService(), orderService := NewOrderService(), orderProcessor := NewOrderProcessor(stockService, orderService) की तरह बना सकते हैं
      initialization का कोई “synchronization” चाहिए ही नहीं होना चाहिए, और गलती होने पर code compile नहीं होगा। अगर circular dependency जोड़ दें, तो उसे सही क्रम में compose नहीं किया जा सकेगा, इसलिए वह साफ़ दिखाई देगा
  • यह बहुत अच्छा लेख है जिसमें कई दिलचस्प ideas हैं। यक़ीन नहीं होता कि मुझे signal.NotifyContext के बारे में पता नहीं था
    अब हर project में copy-paste किए बिना भी signal handling का तरीका याद रख सकूँगा

  • यहाँ दिया गया एक तरीका मुझे काफ़ी पसंद आया, लेकिन मेरे tests थोड़े अलग हैं
    newTestServer() में fake dependencies डालकर server चलाता हूँ, और अगर dependency error test करनी हो तो उस property को error लौटाने वाले fake से बदल देता हूँ
    इस तरह error path, log entries, metrics emission, timeout, और graceful shutdown तक verify किया जा सकता है
    server शुरू होने के बाद यह भी देखता हूँ कि वह किस port पर bind हुआ है। default :0 होने की वजह से वास्तव में assigned port का इंतज़ार करना पड़ता है
    “unit” tests handler level या HTTP level पर किए जा सकते हैं, और पूरे middleware से गुज़ारते हुए या बिल्कुल बिना middleware के भी code को उसी तरह काफ़ी test किया जा सकता है जैसा user उसे देखेगा। N instances चलाकर parallel tests करना भी संभव है

  • मैं Go इस्तेमाल नहीं करता, फिर भी ये patterns पसंद आए। testable code पर ये काफ़ी broadly लागू होते लगते हैं
    dependencies को implicit/static/test न की जा सकने वाली चीज़ की तरह संभालने वाली चीज़ें, खासकर Python quick-start guides, अब फिर नहीं देखना चाहता