1 पॉइंट द्वारा GN⁺ 2023-09-20 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 की जा सकती है
    • Example program: Go Playground example
    • यह comment केवल Go Playground में लागू होता है
  • 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 में संकलित हैं

और पढ़ें

  • बदलाव की details design document और FAQ में देखी जा सकती हैं

1 टिप्पणियां

 
GN⁺ 2023-09-20
Hacker News की रायें
  • इससे कहीं पुराने उदाहरण भी हो सकते हैं, लेकिन 60 सेकंड की खोज में इस व्यवहार के बारे में मिला सबसे पुराना चेतावनी-संदेश 30 साल से भी पहले, 1992 में पोस्ट किया गया comp.lang.lisp FAQ था
    उसमें समझाया गया है कि DOTIMES, DOLIST, DO iteration variable को अपडेट करते समय binding नहीं बल्कि assignment इस्तेमाल करते हैं, इसलिए उदाहरण की तरह अगर lambda n को capture करता है, तो सभी 10 closures उसी एक variable N के value पर बनाए जाते हैं

    • D में भी यही issue है: https://issues.dlang.org/show_bug.cgi?id=2043
      अगर reference से capture कर रहे हैं, तो असल में यही अपेक्षित व्यवहार है
    • standard में यह स्पष्ट नहीं है कि ऐसे loops value को बदलते हैं या rebind करते हैं, इसलिए अगर variable capture कर रहे हों तो मानना चाहिए कि वे rebind नहीं करते
      फिर भी, एक बार इसका काम करने का तरीका सीख लेने के बाद यह समस्या नहीं रहती, और जरूरत पड़े तो 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 को भी कई सालों तक यही feature request कई बार मिली, लेकिन जवाब हमेशा यही रहा कि “फायदा कम है और मौजूदा code टूटेगा”: https://discuss.python.org/t/make-lambdas-proper-closures/10...
      Python 2 से 3 में जाते समय सिर्फ string type बदलने से जो हंगामा हुआ था, उसे देखते हुए यह बदलाव Python 4.0 से पहले आते नहीं लगता
      और कोई न कोई पहले कहेगा कि Python खराब है क्योंकि वह ऐसी चीजें ठीक नहीं करता, फिर 2003 में बनाई गई script नहीं चलने पर फिर Python को कोसेगा
    • C# team के jaredpar ने इस Go proposal की GitHub discussion में पहला comment किया था: https://github.com/golang/go/discussions/56010
      मुझे लगता है कि language change proposals में सामान्य तौर पर मौजूद “पहले reject करो” वाली बाधा पार करने में इसका बड़ा योगदान था
      एक और बात जिसने काफी मनाया, वह थी public source codebases को scan कर के यह देखना कि ठीक होने वाले bugs और नए पैदा होने वाले bugs का संतुलन कैसा है
    • Java में भी anonymous classes के साथ यह समस्या थी, और आम तौर पर function object introduce कर के इसे हल किया जाता है
      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 किए जाते हैं, तब क्या होगा यह जानने की उत्सुकता है
    • JavaScript में भी यही समस्या थी और उसने for(let) loops introduce किए
    • Go के अंदाज में, पिछली languages से सीखे बिना इस behavior को ignore किया गया, और बाद में फिर इसे ठीक करने की कोशिश की जा रही है
  • https://eli.thegreenplace.net/2019/go-internals-capturing-lo... लगता है इस समस्या को और विस्तार से समझाता है

    • यह दिलचस्प है कि पुरानी i := i trick के काम करने की वजह मेरे सोचे से बिल्कुल अलग थी
      शुरू में मुझे लगा था कि नया i goroutine में 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 किया जाए
      नया i for body के scope में है और loop खुद उसे update नहीं करता, इसलिए उसे initialization के बाद update न होने वाली value माना जाता है, और heap allocation के बिना value से capture करने वाला code generate होता है
      यह तो समझता हूं कि दूसरा तरीका बेहतर है, लेकिन Go को गहराई से जानने वाले किसी व्यक्ति से सुनना चाहूंगा कि पहला तरीका साथ-साथ क्यों नहीं होता
  • क्या यह बदलाव उन प्रोग्रामों को तोड़ नहीं देगा जो मौजूदा व्यवहार पर निर्भर हैं?

    • मौजूदा code के साथ backward compatibility सुनिश्चित करने के लिए नए semantics सिर्फ उन modules के packages पर लागू होंगे जिनमें go.mod में go 1.22 या उससे ऊपर घोषित है
      फ़ाइल-स्तर पर इसे //go:build लाइन का उपयोग करके भी तय किया जा सकता है
    • पता नहीं downvote क्यों मिल रहे हैं, लेकिन असल में यह Go 1 compatibility promise को तोड़ने वाला बदलाव ही है
      वह वादा कहता है कि Go 1 specification के तहत लिखे गए programs, specification की lifetime के दौरान बिना बदलाव के compile होते रहें और सही तरीके से चलते रहें; और भले ही कभी Go 2 specification आ सकती है, तब तक Go 1.1, Go 1.2 जैसे point releases में भी जो Go program आज काम करता है, वह काम करता रहना चाहिए
    • Go 1.21 की तैयारी के दौरान बहुत बड़े Go code corpus का analysis करके देखा गया कि क्या प्रभावित होगा, और कहा गया कि संख्या बहुत-बहुत कम थी
      उनका मानना था कि इस design की वजह से अनजाने में bug बनाने वालों की संख्या, fix से प्रभावित होने वालों की संख्या से कहीं ज्यादा होगी
    • मूल proposal में इस syntax के मौजूदा use cases की जांच को काफी विस्तार से कवर किया गया था
      याद पड़ता है कि 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]() 2 print करता है

    • यह सही behavior है
      Python पहले और भी खराब था, और list comprehension के बाहर की scope तक share करता था
    • यह behavior Python closures की late binding की वजह से है
      list comprehension या loop के अंदर lambda इस्तेमाल करने पर वह x की current value नहीं, बल्कि variable x का reference capture करता है
      जब funcs[0]() call होता है, तब तक x पहले ही range की आखिरी value 2 पर set हो चुका होता है
      अगर desired behavior चाहिए, तो lambda के default argument के रूप में x pass करें: funcs = [(lambda x=x: x) for x in range(3)]
  • मैंने Go थोड़ा ही इस्तेमाल किया है और इस बदलाव से solve होने वाली आम समस्या समझता हूं, लेकिन letsencrypt वाला ज्यादा subtle उदाहरण या "range c.informerMap" बनाम "range alarms" ठीक से समझ नहीं आ रहा
    for k, v := range someMap में v map value type होता है, और पूरे loop में एक ही binding होती है जिसमें हर iteration पर copy होती है? अगर ऐसा है तो समस्या समझ आती है, लेकिन मैंने उम्मीद की थी कि v map के अंदर की चीज़ की ओर point करने वाला reference होगा
    specification के “For statements with range clause” को जल्दी से देखने पर भी जवाब नहीं मिला, शायद Go से बहुत कम वास्ता होने की वजह से गलत जगह देख रहा था: https://go.dev/ref/spec#For_statements
    edit: जवाब code block format वाली table में था। शायद banner की तरह नज़रअंदाज़ कर गया। यह जानकर हैरानी हुई कि v reference नहीं बल्कि copied value है

    • Go map keys या values के pointers support नहीं करता
      array slots के pointers support करता है, लेकिन for range हर slot की ओर point करने वाला pointer देने के बजाय copy करता है
    • अगर string से integer तक जाने वाला map है, तो v का type int है
      यह value है, int का pointer नहीं
    • इन code snippets का source मिल गया
      अगर जिज्ञासा हो तो देख सकते हैं: 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.mod file के 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 में manually go 1.21 लिखें, तो बिना शिकायत build हो जाता है
    • दिलचस्प बात यह है कि Go 1.21 में अगर module कोई higher Go version declare करता है, तो default behavior एक नया toolchain लाकर उसकी जगह use करना है: https://go.dev/blog/toolchain
      काफ़ी अच्छा 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 में 1.22 module dependency के रूप में आए तो भी compile हो जाएगा, और अगर वह इस feature पर depend करता है तो गलत logic बना सकता है
      इसलिए Go 1.18 का उपयोग actively dangerous हो जाता है
      Go 1.19 में compiler error आएगा
      वैसे भी Go old releases और standard library में security bug fixes नहीं करता, इसलिए ऐसे versions का इस्तेमाल अपने-आप में risky है
    • compile error आना चाहिए
      लेकिन Go 1.22 से compile करने पर भी तुम्हारे code में अभी भी Go 1.18 semantics ही रहेंगी
  • Go कुछ मायनों में बहुत अजीब language है
    यह बेहद strongly opinionated language है, और साथ ही ऐसी लगती है जैसे इसकी राय बहुत कम हो

  • c.informerMap पर iterate करने वाले code और alarms पर iterate करने वाले code में क्या अंतर है, यह पूरी तरह साफ़ नहीं है, लेकिन अनुमान लगाऊँ तो एक तरफ़ loop variable pointer हो सकता है और दूसरी तरफ़ value
    method call pointer receiver इस्तेमाल करता है, इसलिए value के case में compiler receiver का reference अपने-आप डाल देता होगा?

    • GitHub code search से इस code snippet वाला original मिल गया
      https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
      https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
      अंतर यह है कि एक तरफ़ informer interface है, इसलिए method call तुरंत informer.Run के रूप में resolve हो जाता है और समस्या नहीं होती
      दूसरी तरफ़ a Alarm struct है और value के रूप में copy होता है, जबकि Monitor method pointer receiver लेता है
      इसलिए compiler असल में go a.Monitor(b) को go (&a).Monitor(b) में बदल देता है, और यह loop variable का reference बनाकर समस्या पैदा करता है
    • Go में map पर iterate करने पर value हमेशा copy होती है, इसलिए पहला code उम्मीद के मुताबिक़ काम करता लगता है
      दूसरे में अनुमान है कि a आखिरकार alarms के last element की value ही रखता है, इसलिए article में बताया गया original problem पैदा होता है
    • नाम देखकर लगता है कि ऊपर वाला map है और नीचे वाला slice
      मेरी internal knowledge बस इतनी ही है, लेकिन slice में heap का backing array होता है, इसलिए pointer या reference कुछ हद तक जुड़े होते हैं
    • compiler में values को capture करने का पता रखने जैसी कोई चीज़ ज़रूर लगती है
  • यह पढ़कर बहुत राहत मिली
    Go की सबसे बड़ी खामी में से एक ठीक हो रही है

    • नहीं, सबसे बड़ी खामी error handling है
      foo, err := getFoo(); if err != nil ... के बाद अगर bar, err := getBar(); fmt.Println(bar) जैसा लिखें, तो getBar की error check छूट जाती है
      scoping rules की वजह से if foo, err := getFoo(); err != nil pattern nesting थोड़ी भी गहरी होते ही संभालना मुश्किल हो जाता है
      यह invalid state भी introduce करता है। जब getFoo error return करता है तो उसे क्या return करना चाहिए? API को pointer return में बदलकर nil return करें, या invalid state वाला partially created object रखें—यह चिंता करनी पड़ती है
    • अगली बार interfaces की nil check ठीक कर दें