13 साल बाद Go में HTTP सेवाएं लिखने का तरीका
(grafana.com)- लंबे समय तक चलने वाली Go HTTP सेवाओं को स्पष्ट dependency passing, एक जगह इकट्ठे routes, और testable
runfunction के साथ बनाया जाए तो 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.Oncelazy initialization repetitive code को घटाते हैं, फिर भी Go standardnet/httpflow बनाए रखते हैं - tests में individual handlers के बजाय वास्तविक API calls के करीब end-to-end approach को प्राथमिकता दी जाती है, और हर test अपना server चलाकर
/healthzया/readyzसे readiness check करता है
Server creation और service entry point
NewServerconstructor को service का corehttp.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 पैदा कर सकने वाले काम पहले
runfunction में handle करें - handler registration चरण में
mux.Handle,mux.HandleFunc,http.NotFoundHandlerजैसे routing पर focus करें - अगर design में handler को खुद error return करना है, तो
addRoutesभी error return कर सकता है
- error पैदा कर सकने वाले काम पहले
main से सिर्फ run call करना
func main()कोrun()call करने वाला, error होने परstderrमें लिखकर abnormal exit करने वाला पतला function रखेंrunoperating system के basic elements जैसेcontext.Context, arguments, input/output, environment access functions को arguments के रूप में लेता हैrunerror 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अगरnilreturn करे तो normal exit होता है- error return करने पर
mainerror 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) stringinject करके 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 पर
Shutdowncall करके gracefully रुकता है- example में अलग goroutine में
ctx.Done()का wait किया जाता है - shutdown context में
10 * time.Secondtimeout रखा जाता है - shutdown के दौरान error हो तो
stderrमें record करें
- example में अलग goroutine में
- tests में server सच में ready है या नहीं check करने के लिए
/healthzया/readyzendpoints रखें- अलग channel से readiness signal बनाया जा सकता है, लेकिन actual HTTP request से check करने का तरीका पसंद है
- readiness check loop
200 OKआने तक requests करता है - context cancellation या timeout तक पहुंचने पर error return करता है
- example loop requests के बीच
250mssleep करता है
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/decodehelpers रखें- example JSON
Content-Typeset करता है, 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 संभव है decodereturn type है, इसलिएdecode[CreateSomethingRequest](https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/r)की तरह expected type specify करना होगा
- example JSON
- validation के लिए single-method interface इस्तेमाल करें
Validatorinterface का shapeValid(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]typeTको अनिवार्य रूप सेValidatorimplement करने के लिए enforce करता है nilmap परlen(problems)call करने पर भी 0 आता है, इसलिए panic नहीं होता
Middleware adapter pattern
- middleware
http.Handlerलेता है और नयाhttp.Handlerreturn करता है- original handler call से पहले/बाद code run कर सकता है
- condition के आधार पर original handler को call न भी कर सकता है
- example का
adminOnlyadmin न होने परHTTP 404 Not Foundreturn करता है और 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.Handlerreturn करता है- route registration code में
middleware(handleSomething(...))जैसे concise तरीके से इस्तेमाल करें - अलग
type middleware func(h http.Handler) http.Handlerdefine किया जा सकता है, लेकिन 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 दिखाते हैं
- उदाहरण के लिए
/greetendpoint को पूरेPersonकी नहीं, सिर्फNamefield की जरूरत है, तो 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.Onceguarantee करता है कि 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 मानता है
runfunction 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 के करीब है
runcall करके 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.Cleanupdirectdeferके विकल्प के रूप में इस्तेमाल होता है
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 टिप्पणियां
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 कॉल करना पड़ेगा
इसके बजाय अगर
Usernametype औरNewUsername(username string) (Username, error)constructor हो, तो सिर्फUsernameobject के मौजूद होने से ही यह गारंटी मिल जाती है कि वह validation पास कर चुका है[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
stringकी तरह पास करने के बजाय, उसे parse करके type देना चाहिएयह 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/
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 में नहीं, दूसरी भाषाओं में भी काफ़ी परेशान करने वाला है
Python projects में मैं immutable config
dataclassका काफ़ी उपयोग करता हूँ और उसे कई modules में पास करता हूँ। जब कई functions कई values पर निर्भर हों, तो हर value को function argument के रूप में पास करने और types अलग से define करने के बजाय, एक हीdataclassमें सारे variables और type definitions होने से यह काफ़ी सुविधाजनक design pattern बन जाता हैOptionfunctions से उसे composable रखनाअंदरूनी
optionsको सिर्फ construction के दौरान बदला जाए, और बाहर सिर्फConfigऔर accessors expose किए जाएँ। उदाहरण के लिएconfig.New(config.Name("Emanon"))की तरह बनाया जा सकता है औरcfg.Name()से पढ़ा जा सकता हैConfigstruct बनाता हूँ, औरconfigs.Configको उन package-स्तरीयConfigका संग्रह मानता हूँयह शायद Go best practice न हो, लेकिन शुरुआत में पूरे system configuration को एक entity की तरह बनाया जा सकता है, और हर package को सिर्फ उसकी न्यूनतम आवश्यक dependencies दी जा सकती हैं, इसलिए यह अच्छा लगता है
testing के समय भी सिर्फ एक package test करने के लिए पूरा configuration fake बनाने की ज़रूरत नहीं पड़ती, इसलिए थोड़ी आसानी हो जाती है
उसे कभी modify नहीं करना चाहिए। यह पता नहीं चलता कि कहाँ और कैसे इस्तेमाल हो रहा है, इसलिए इसमें मानव-घंटों की बर्बादी बहुत महँगी पड़ती है; अगर बदलाव चाहिए, तो मूल से derived value बनाना बेहतर है
मज़ेदार बात यह है कि design के हिसाब से config object कुछ हद तक immutable था, और उसे modify करने के लिए
WARNING_DO_NOT_USEAPI इस्तेमाल करनी पड़ती थी, फिर भी उसी से object बदलकर outage कर दिया गयामुझे 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 में सबसे अच्छा है, लेकिन फिर भी लगता है कि इससे बेहतर हल होना चाहिए
जहाँ संभव हो, plugins अच्छे code boundary strategy होते हैं। plugin architecture मूल रूप से opt-out नहीं बल्कि opt-in होता है, इसलिए वह हर संभावना को किसी एक code blob पर थोपता नहीं है
मैं इस तरह के software को “à la carte” कहता हूँ। सामान्य तौर पर “कुछ भी करने के लिए सब कुछ करने” वाली स्थिति से बचना चाहिए
userspackage याcommentspackage जैसा रखताइन packages का कोई HTTP interface नहीं होता, लेकिन हर एक का अपना
mainऔर किसी तरह का CLI interface होता है। उस file comment में//go:build ignoreउपयोगी रहता हैfunc HandleX(w http.ResponseWriter, req *http.Request)की तरह handler define करने के बजाय,func HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFuncके रूप में ज़रूरी dependencies लेता हूँ और अंदर से असलीhttp.HandlerFuncreturn करता हूँऔर फिर 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 संभव बनाते हैं, लेकिन उनके भी नुकसान हैं। फिर भी यह संभाली जा सकने वाली एक संरचना है, कोई गलती नहीं
मुख्य बात यह है कि
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 गायब नहीं होतीं — उन्हें बस
serverclass/struct में छिपा दिया जाता है, और method signature छोटा होने से ऐसा दिखाया जाता है मानो dependencies कम होंसमय के साथ मुझे लगता है कि अगर 20 dependencies भी हों, तब भी वह code बेहतर है जिसमें सभी dependencies function/method signature में साफ़ दिखाई दें। इससे code complexity बढ़ने की बात छिपाई नहीं जाती
उदाहरण के लिए
CreateUserstruct के अंदर सिर्फ उसी काम के लिए ज़रूरी dependencies जैसेstore,cache,logger,pubरखे जाते हैं औरServeHTTPimplement किया जाता हैmain.goया जहाँ dependencies setup की जाती हैं, वहाँ हर action बनाया जाता है और केवल ज़रूरी dependencies पास की जाती हैं। इससे किसी खास action/handler की helper methods को उसी struct की private methods के रूप में रखना अच्छा लगता हैलेकिन अगर कोई action किसी दूसरे action को ज़रूरी मानने लगे, तो उन्हें एक-दूसरे को pass करना शुरू करना पड़ता है या फिर अलग package/service में निकालना पड़ता है, जो झंझट बन सकता है
उदाहरण के लिए
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 में यह उतना उपयोगी नहीं लगता।
runfunction को structured config देना बेहतर होगा ताकि tests ज़्यादा focused रहेंhandlers में
sync.Onceसे templates parse करने के तरीके से मैं सहमत नहीं हूँ। मेरा मानना है कि handler को template parsing नहीं करनी चाहिए; यह app startup के समय होनी चाहिए। अगर templates parse नहीं हो सकते, तो app को requests लेने के लिए ready state में नहीं आना चाहिए और उसे non-zero exit code के साथ समाप्त हो जाना चाहिएहाल में ogen के साथ प्रयोग कर रहा हूँ: https://github.com/ogen-go/ogen
OpenAPI definition लिखने पर यह routing, struct definition, JSON schema validation आदि संभाल लेता है। मुझे बस service implement करनी होती है
query string integer range validation जैसी चीज़ें बहुत उबाऊ होती हैं, और खुद लिखो तो typo करना भी बहुत आसान है
अभी बस इसे आज़माने के चरण में हूँ, इसलिए अभी तक इसकी कोई खराबी नहीं मिली है
Protobuf, capnproto जैसी मिलती-जुलती IDL लिखना कहीं ज़्यादा productive लगता है
[1] https://github.com/danielgtaylor/huma
[2] https://github.com/swaggest/rest
लगा था कि 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 अब तक सबसे ज़्यादा पसंद आई है
अच्छे 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, अब फिर नहीं देखना चाहता