- Go 1.22 में
for लूप variables को पूरे लूप के बजाय हर iteration के अलग scope में बदला जा रहा है, ताकि closure द्वारा उसी variable को गलत तरीके से capture करने वाली Go की आम गलती कम हो
- मौजूदा semantics में, goroutine न होने पर भी iteration के बाद चलने वाला function उसी
v या i को reference करता है, जिससे सिर्फ आखिरी value दिख सकती है या test गलत तरीके से pass हो सकता है
go vet और gopls का loopclosure analyzer केवल पक्के मामलों को पकड़ता है, इसलिए misses होते हैं; और ज्यादा aggressive checkers false positives के कारण अनावश्यक x:= x code बढ़ा सकते हैं
- नई semantics केवल उन modules पर लागू होगी जिनके
go.mod में go 1.22 या उससे ऊपर declare है; Go 1.21 में GOEXPERIMENT=loopvar से preview चलाया जा सकता है
- Google ने मई 2023 की शुरुआत से अपने internal Go toolchain में इस mode को सभी builds के लिए force किया, और 4 महीनों में production issue की कोई report नहीं आई, लेकिन गलत लिखे गए tests सामने आए
मौजूदा for लूप में variable capture का trap
- Go के मौजूदा
for लूप variables का scope पूरे लूप में होता है, इसलिए iteration खत्म होने के बाद उस variable को reference करने वाला code इरादे से अलग value देख सकता है
values := []string{"a", "b", "c"} पर iterate करते हुए तीन goroutine बनाएं, तो हर goroutine iteration-specific v के बजाय उसी एक variable v को print करती है
- Concurrency के बिना भी यही समस्या आती है
- अगर iteration के अंदर
func() { fmt.Println(i) } को slice में store करके बाद में चलाया जाए, तो हर function iteration-specific value के बजाय उसी i को reference करता है
Production outages और analyzers की सीमाएं
- ऐसी गलतियां कई कंपनियों में production issues का कारण बनीं, और Let’s Encrypt का public issue भी उनमें से एक है
- Let’s Encrypt के मामले में, map iteration के दौरान
k को kCopy := k से copy किया गया था, लेकिन modelToAuthzPB(&v) result बनाने की प्रक्रिया में v के field pointers इस्तेमाल करता था, इसलिए v को भी अलग से copy करना जरूरी था
- variable capture कई functions में फैला था, इसलिए समस्या पहचानना मुश्किल था
- Static analysis tools के लिए यह तय करना मुश्किल है कि variable iteration के बाद तक जीवित रहता है या नहीं, इसलिए उन्हें false positives और misses के बीच trade-off करना पड़ता है
go vet और gopls का loopclosure analyzer केवल पक्की problems report करता है और misses स्वीकार करता है
- ज्यादा aggressive checkers सही code को भी गलत code के रूप में flag कर सकते हैं
- Open-source Go code में
x := x line जोड़ने वाले commits देखने पर, असली bug fixes के साथ कई अनावश्यक changes भी मिले
- ऐसी स्थितियां थीं जहां developers checker को satisfy करने के लिए अनावश्यक code जोड़ रहे थे
informer := informer और a := a जैसे दो diffs में सिर्फ एक bug fix था और दूसरा अनावश्यक change, लेकिन type और function जानकारी के बिना फर्क करना मुश्किल है
Go 1.22 की नई loop semantics
- Go 1.22 में
for loop variables को हर iteration में अलग scope देने के लिए बदला जाना तय है
- ऊपर के examples अब bug वाले Go programs नहीं रहेंगे, और ऐसी गलतियों से होने वाले production issues तथा inaccurate checking tools की जरूरत भी कम होगी
- Backward compatibility के लिए नई semantics केवल उन modules के packages पर लागू होगी जिनके
go.mod में go 1.22 या उससे ऊपर declare है
- पूरे codebase को एक बार में बदले बिना gradual migration संभव है
//go:build line से file-level control भी संभव है
- मौजूदा code अभी जैसी semantics है वैसी ही बनाए रखेगा
- बदलाव केवल नए code या updated code पर लागू होगा
- किसी specific package में semantics कब बदलेगी, इसे developer control कर सकता है
पुराने Go versions में safeguards
- Go के forward compatibility work के अनुसार Go 1.21,
go 1.22 या उससे ऊपर declare करने वाले code को compile नहीं करता
- Go 1.20.8 और Go 1.19.13 point releases में भी वही effect देने वाली special handling शामिल की गई
- Go 1.22 release होने के बाद नई semantics पर निर्भर होकर लिखा गया code, बहुत पुराने end-of-support Go versions का इस्तेमाल न करने पर, मौजूदा semantics के साथ compile नहीं होगा
Go 1.21 में preview चलाना
- Go 1.21 में loop scope change का preview शामिल है
GOEXPERIMENT=loopvar set करके compile करने पर go.mod की go line ignore होती है और सभी loops पर नई semantics लागू होती है
- यह check करने के लिए कि package और उसकी सभी dependencies नई loop semantics में भी tests pass करती हैं या नहीं, इसे ऐसे चलाएं
GOEXPERIMENT=loopvar go test
- Go Playground में program के top पर
// GOEXPERIMENT=loopvar comment डालकर नई semantics test की जा सकती है
- Google के internal Go toolchain को मई 2023 की शुरुआत से सभी builds में इस mode को force करने के लिए patch किया गया था, और अगले 4 महीनों में production code issues की कोई report नहीं आई
नई semantics से सामने आने वाले test bugs
- नई loop semantics ने production code problems नहीं पैदा कीं, लेकिन गलत तरीके से pass हो रहे tests को सामने लाया
t.Parallel इस्तेमाल करने वाले subtest example में, Go 1.21 हर subtest को पूरे loop के खत्म होने तक रोकता है और फिर उन्हें parallel में चलाता है
- loop खत्म होने पर
v हमेशा 6 होता है, इसलिए सभी subtests check करते हैं कि 6 even है या नहीं, और pass हो जाते हैं
- असली test cases में
1 है, इसलिए test fail होना चाहिए
- Go 1.21 में
loopclosure analyzer की precision बेहतर की गई है, जिससे यह issue identify और report किया जा सकता है
- Go Playground report example: program example
- अगर
go vet tests में ऐसी problem report करता है, तो उसे fix करना Go 1.22 की तैयारी में मददगार होगा
- नई semantics लागू होने पर किसी specific test failure का कारण बनने वाले loop को खोजने के tools और examples FAQ में संकलित हैं
और पढ़ें
1 टिप्पणियां
Hacker News की रायें
इससे कहीं पुराने उदाहरण भी हो सकते हैं, लेकिन 60 सेकंड की खोज में इस व्यवहार के बारे में मिला सबसे पुराना चेतावनी-संदेश 30 साल से भी पहले, 1992 में पोस्ट किया गया comp.lang.lisp FAQ था
उसमें समझाया गया है कि
DOTIMES,DOLIST,DOiteration variable को अपडेट करते समय binding नहीं बल्कि assignment इस्तेमाल करते हैं, इसलिए उदाहरण की तरह अगरlambdanको capture करता है, तो सभी 10 closures उसी एक variableNके value पर बनाए जाते हैंअगर reference से capture कर रहे हैं, तो असल में यही अपेक्षित व्यवहार है
फिर भी, एक बार इसका काम करने का तरीका सीख लेने के बाद यह समस्या नहीं रहती, और जरूरत पड़े तो form चुनकर macro expand कर के implementation का तरीका देखा जा सकता है
C# language team ने भी C# 4.0 में lightweight closures लाने के बाद यही समस्या झेली, और जल्द ही साफ हो गया कि यह एक trap है
users लगभग हमेशा loop variable का गलत इस्तेमाल करते थे, और C# 5.0 में compatibility तोड़ने वाला बदलाव डाला गया
Eric Lippert ने उस नजरिए से “क्यों” को अच्छी तरह समझाने वाला लेख लिखा था: https://ericlippert.com/2009/11/12/closing-over-the-loop-var...
C# 5 की मूल घोषणा वाली पोस्ट ढूंढना मुश्किल था; उम्मीद है कि 2012 के बाद Microsoft domain पर कई blog migrations के दौरान वह गायब नहीं हुई होगी
Python 2 से 3 में जाते समय सिर्फ string type बदलने से जो हंगामा हुआ था, उसे देखते हुए यह बदलाव Python 4.0 से पहले आते नहीं लगता
और कोई न कोई पहले कहेगा कि Python खराब है क्योंकि वह ऐसी चीजें ठीक नहीं करता, फिर 2003 में बनाई गई script नहीं चलने पर फिर Python को कोसेगा
मुझे लगता है कि language change proposals में सामान्य तौर पर मौजूद “पहले reject करो” वाली बाधा पार करने में इसका बड़ा योगदान था
एक और बात जिसने काफी मनाया, वह थी public source codebases को scan कर के यह देखना कि ठीक होने वाले bugs और नए पैदा होने वाले bugs का संतुलन कैसा है
pass-by-value होने के कारण यह call के समय variable की state capture करता है और code की अस्पष्टता कम करता है
अगर variables को अजीब तरह से capture करने की कोशिश करें, तो उदाहरण के लिए array को map में बदलने के लिए accumulate की जा रही collection और घोषित variables अलग-अलग तरह से behave करने लगते हैं
लगता है Go सिर्फ loop counter पर यह behavior लागू कर के संतुलन साधना चाहता है, लेकिन फिर भी कुछ variables अजीब तरह से behave करेंगे
खासकर जब input को सीधे scan करने के लिए कई loop variables define किए जाते हैं, तब क्या होगा यह जानने की उत्सुकता है
for(let)loops introduce किएhttps://eli.thegreenplace.net/2019/go-internals-capturing-lo... लगता है इस समस्या को और विस्तार से समझाता है
i := itrick के काम करने की वजह मेरे सोचे से बिल्कुल अलग थीशुरू में मुझे लगा था कि नया
igoroutine में pass होता है, इसलिए escape analysis इसे lexical scope के बाहर escape करता हुआ mark करती है, इसलिए यह heap पर allocate होता है, और हर iteration में एक heap allocation बनता है जिससे हर goroutine अपनी unique memory location को refer करता हैअसल में Go compiler के पास reference capture और value capture चुनने की heuristics हैं, और एक condition है कि initialization के बाद update न होने वाली value को value से capture किया जाए
नया
iforbody के scope में है और loop खुद उसे update नहीं करता, इसलिए उसे initialization के बाद update न होने वाली value माना जाता है, और heap allocation के बिना value से capture करने वाला code generate होता हैयह तो समझता हूं कि दूसरा तरीका बेहतर है, लेकिन Go को गहराई से जानने वाले किसी व्यक्ति से सुनना चाहूंगा कि पहला तरीका साथ-साथ क्यों नहीं होता
क्या यह बदलाव उन प्रोग्रामों को तोड़ नहीं देगा जो मौजूदा व्यवहार पर निर्भर हैं?
go.modमेंgo 1.22या उससे ऊपर घोषित हैफ़ाइल-स्तर पर इसे
//go:buildलाइन का उपयोग करके भी तय किया जा सकता हैवह वादा कहता है कि Go 1 specification के तहत लिखे गए programs, specification की lifetime के दौरान बिना बदलाव के compile होते रहें और सही तरीके से चलते रहें; और भले ही कभी Go 2 specification आ सकती है, तब तक Go 1.1, Go 1.2 जैसे point releases में भी जो Go program आज काम करता है, वह काम करता रहना चाहिए
उनका मानना था कि इस design की वजह से अनजाने में bug बनाने वालों की संख्या, fix से प्रभावित होने वालों की संख्या से कहीं ज्यादा होगी
याद पड़ता है कि Google codebase या GitHub code में ऐसे मामले लगभग नहीं थे जहां यह बदलाव expected behavior तोड़ता हो
प्रभावित codebases कितने कम हैं यह पुष्टि करने, और
go.modकी version declaration के जरिए नया behavior इस्तेमाल करने के लिए code को सक्रिय रूप से modify करना पड़े ऐसा mechanism बनाने के बाद ही backward compatibility तोड़ने का फैसला लिया गयाhttps://twitter.com/go100and1/status/1690412229135601664
https://twitter.com/go100and1/status/1690587305806057472
https://twitter.com/go100and1/status/1690589791686119424
https://twitter.com/go100and1/status/1690591234715492352
https://twitter.com/go100and1/status/1690593184857145344
https://twitter.com/go100and1/status/1691456732151889920
इनमें से ज्यादातर का proposal document में बिल्कुल उल्लेख नहीं था
Python में भी मुझे यह समस्या हुई है, लेकिन हाल में नहीं
पक्का नहीं कि Python बदला है, या मैं समस्या पहचानने लगा हूं
यह अभी भी Python में समस्या हो सकती है, यह इस code से ही साफ दिखता है:
funcs = [(lambda: x) for x in range(3)];funcs[0]()2print करता हैPython पहले और भी खराब था, और list comprehension के बाहर की scope तक share करता था
list comprehension या loop के अंदर lambda इस्तेमाल करने पर वह
xकी current value नहीं, बल्कि variablexका reference capture करता हैजब
funcs[0]()call होता है, तब तकxपहले हीrangeकी आखिरी value2पर set हो चुका होता हैअगर desired behavior चाहिए, तो lambda के default argument के रूप में
xpass करें:funcs = [(lambda x=x: x) for x in range(3)]मैंने Go थोड़ा ही इस्तेमाल किया है और इस बदलाव से solve होने वाली आम समस्या समझता हूं, लेकिन letsencrypt वाला ज्यादा subtle उदाहरण या
"range c.informerMap"बनाम"range alarms"ठीक से समझ नहीं आ रहाfor k, v := range someMapमेंvmap value type होता है, और पूरे loop में एक ही binding होती है जिसमें हर iteration पर copy होती है? अगर ऐसा है तो समस्या समझ आती है, लेकिन मैंने उम्मीद की थी किvmap के अंदर की चीज़ की ओर point करने वाला reference होगाspecification के “For statements with range clause” को जल्दी से देखने पर भी जवाब नहीं मिला, शायद Go से बहुत कम वास्ता होने की वजह से गलत जगह देख रहा था: https://go.dev/ref/spec#For_statements
edit: जवाब code block format वाली table में था। शायद banner की तरह नज़रअंदाज़ कर गया। यह जानकर हैरानी हुई कि
vreference नहीं बल्कि copied value हैarray slots के pointers support करता है, लेकिन
for rangeहर slot की ओर point करने वाला pointer देने के बजाय copy करता हैvका typeintहैयह value है,
intका pointer नहींअगर जिज्ञासा हो तो देख सकते हैं: https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
मूल रूप से compiler automatic dereferencing की वजह से
go a.Monitor(b)को(&a).Monitor(b)में बदल रहा है“फ़ॉरवर्ड compatibility के काम के नतीजे के तौर पर Go 1.21
go 1.22या उससे ऊपर घोषित करने वाले code को compile करने की कोशिश नहीं करता। Go 1.20.8 और Go 1.19.13 point releases में भी इसी असर की special handling डाली गई है, इसलिए Go 1.22 release होने पर नई semantics पर निर्भर होकर लिखा गया code, जब तक बहुत पुराना unsupported Go version इस्तेमाल न किया जा रहा हो, कभी भी पुरानी semantics के साथ compile नहीं होगा” — यह हिस्सा कैसे काम करता है, यह जानने की उत्सुकता हैअगर कोई package 1.22 पर pinned है और मैं 1.18 से compile करूँ, तो क्या compile होगा या यह error देगा कि 1.22 compiler चाहिए?
Go 1.21 में
go.modfile के version number format को बदला गया, इसलिए Go 1.18 से build करने की कोशिश करें तोgo.mod:3: invalid go version '1.21.0': must match format 1.23जैसा error आता हैहालांकि यह सिर्फ तब होता है जब module
go mod initसे बनाया गया हो; अगरgo.modमें manuallygo 1.21लिखें, तो बिना शिकायत build हो जाता हैकाफ़ी अच्छा feature है, लेकिन behavior चौंकाने वाला है, और binary download करने के लिए Google-controlled server से connect करने वाली बात थोड़ी झिझक पैदा करती है
module proxy के साथ यह Go की सबसे ambivalent features में से एक है, और अगर Go को ऐसी foundation manage करती जिसमें Google की सिर्फ हिस्सेदारी होती, तो मैं कहीं ज़्यादा सहज महसूस करता
edit: सोचने पर लगा कि यह dependency के कोई दूसरा version declare करने पर नहीं, बल्कि current module के declare करने पर लागू बात है, इसलिए original question से अलग है
इसलिए Go 1.18 का उपयोग actively dangerous हो जाता है
Go 1.19 में compiler error आएगा
वैसे भी Go old releases और standard library में security bug fixes नहीं करता, इसलिए ऐसे versions का इस्तेमाल अपने-आप में risky है
लेकिन Go 1.22 से compile करने पर भी तुम्हारे code में अभी भी Go 1.18 semantics ही रहेंगी
Go कुछ मायनों में बहुत अजीब language है
यह बेहद strongly opinionated language है, और साथ ही ऐसी लगती है जैसे इसकी राय बहुत कम हो
c.informerMapपर iterate करने वाले code औरalarmsपर iterate करने वाले code में क्या अंतर है, यह पूरी तरह साफ़ नहीं है, लेकिन अनुमान लगाऊँ तो एक तरफ़ loop variable pointer हो सकता है और दूसरी तरफ़ valuemethod call pointer receiver इस्तेमाल करता है, इसलिए value के case में compiler receiver का reference अपने-आप डाल देता होगा?
https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
अंतर यह है कि एक तरफ़
informerinterface है, इसलिए method call तुरंतinformer.Runके रूप में resolve हो जाता है और समस्या नहीं होतीदूसरी तरफ़
aAlarmstruct है और value के रूप में copy होता है, जबकिMonitormethod pointer receiver लेता हैइसलिए compiler असल में
go a.Monitor(b)कोgo (&a).Monitor(b)में बदल देता है, और यह loop variable का reference बनाकर समस्या पैदा करता हैदूसरे में अनुमान है कि
aआखिरकारalarmsके last element की value ही रखता है, इसलिए article में बताया गया original problem पैदा होता हैमेरी internal knowledge बस इतनी ही है, लेकिन slice में heap का backing array होता है, इसलिए pointer या reference कुछ हद तक जुड़े होते हैं
यह पढ़कर बहुत राहत मिली
Go की सबसे बड़ी खामी में से एक ठीक हो रही है
foo, err := getFoo(); if err != nil ...के बाद अगरbar, err := getBar(); fmt.Println(bar)जैसा लिखें, तोgetBarकी error check छूट जाती हैscoping rules की वजह से
if foo, err := getFoo(); err != nilpattern nesting थोड़ी भी गहरी होते ही संभालना मुश्किल हो जाता हैयह invalid state भी introduce करता है। जब
getFooerror return करता है तो उसे क्या return करना चाहिए? API को pointer return में बदलकरnilreturn करें, या invalid state वाला partially created object रखें—यह चिंता करनी पड़ती है