- Go 1.22,
for döngüsü değişkenlerini tüm döngü için değil her yineleme için ayrı kapsam olacak şekilde değiştirerek, closure’ların aynı değişkeni yanlış yakalamasından doğan Go’nun tipik hatalarını azaltmayı hedefliyor
- Mevcut anlambilimde, goroutine olmasa bile yinelemeden sonra çalışan bir fonksiyon aynı
v ya da i değişkenine başvurabildiği için yalnızca son değeri görebilir veya testler yanlış şekilde başarılı olabilir
go vet ve gopls içindeki loopclosure çözümleyicileri yalnızca kesin durumları yakaladığından bazı hataları kaçırır; daha agresif denetleyiciler ise yanlış pozitifler nedeniyle gereksiz x := x kodlarını artırabilir
- Yeni anlambilim yalnızca
go.mod içinde go 1.22 veya üzerini bildiren modüllere uygulanacak; Go 1.21’de ise GOEXPERIMENT=loopvar ile önizleme olarak denenebilecek
- Google, 2023 Mayıs’ının başından beri dahili Go araç zincirinde bu modu tüm derlemelerde zorunlu kıldı; 4 ay boyunca üretimde sorun raporlanmadı, ancak hatalı yazılmış testler ortaya çıktı
Mevcut for döngüsünde değişken yakalama tuzağı
- Go’daki mevcut
for döngüsü değişkenleri tüm döngü boyunca tek kapsam kullandığı için, yineleme bittikten sonra o değişkene başvuran kod beklenenden farklı bir değer görebilir
values := []string{"a", "b", "c"} üzerinde dolaşıp üç goroutine oluşturursanız, her goroutine yinelemeye özgü v yerine aynı v değişkenini yazdırır
- Aynı sorun eşzamanlılık olmadan da ortaya çıkabilir
- Döngü içinde
func() { fmt.Println(i) } fonksiyonunu bir slice içine kaydedip daha sonra çalıştırırsanız, her fonksiyon yinelemeye özgü değer yerine aynı i değişkenine başvurur
Üretim kesintileri ve çözümleyicilerin sınırları
- Bu tür hatalar birçok şirkette üretim sorunlarına yol açtı; Let’s Encrypt’in herkese açık sorunu da bunlardan biri
- Let’s Encrypt örneğinde, map üzerinde dolaşırken
k, kCopy := k ile kopyalanmıştı; ancak modelToAuthzPB(&v) sonuç oluştururken v alanlarının pointer’larını kullandığı için v’nin de ayrıca kopyalanması gerekiyordu
- Değişken yakalama birkaç fonksiyona yayıldığından sorunu fark etmek zordu
- Statik analiz araçlarının, bir değişkenin yineleme sonrasına taşıp taşınmadığını belirlemesi zor olduğundan yanlış pozitifler ile kaçırılan hatalar arasında ödün vermesi gerekir
go vet ve gopls içindeki loopclosure çözümleyicileri yalnızca kesin sorunları raporladığı için bazı hataları kaçırmayı göze alır
- Daha agresif denetleyiciler doğru kodu bile hatalı diye işaretleyebilir
- Açık kaynak Go kodlarında
x := x satırı ekleyen commit’lere bakıldığında, gerçek hata düzeltmelerinin yanında çok sayıda gereksiz değişiklik de görülüyordu
- Geliştiriciler, denetleyiciyi memnun etmek için gereksiz kod eklemek zorunda kalabiliyordu
informer := informer ve a := a gibi iki diff’ten biri gerçek bir hata düzeltmesi, diğeri ise gereksiz değişiklikti; ama tür ve fonksiyon bilgisi olmadan bunları ayırt etmek zordu
Go 1.22’nin yeni döngü anlambilimi
- Go 1.22’de
for döngüsü değişkenlerinin her yineleme için ayrı kapsamı olacak şekilde değişmesi planlanıyor
- Yukarıdaki örnekler artık hatalı Go programları sayılmayacak; böylece bu tür hatalardan doğan üretim sorunları ve isabetsiz denetim araçlarına duyulan ihtiyaç da azalacak
- Geriye dönük uyumluluk için yeni anlambilim yalnızca
go.mod içinde go 1.22 veya üzerini bildiren modüllerdeki paketlere uygulanacak
- Tüm kod tabanını tek seferde değiştirmeden kademeli geçiş yapılabilecek
- Dosya bazında denetim için
//go:build satırı da kullanılabilecek
- Mevcut kod, bugünkü anlambilimini aynen koruyacak
- Değişiklik yalnızca yeni ya da güncellenmiş kodlara uygulanacak
- Geliştirici, anlambilimin belirli bir pakette ne zaman değişeceğini kontrol edebilecek
Önceki Go sürümlerindeki güvenlik önlemleri
- Go’nun forward compatibility çalışması doğrultusunda Go 1.21,
go 1.22 veya üzerini bildiren kodu derlemeyecek
- Aynı etkiyi sağlayan özel işlem, Go 1.20.8 ve Go 1.19.13 nokta sürümlerine de eklendi
- Go 1.22 yayımlandıktan sonra, yeni anlambilime dayanarak yazılan kod; çok eski destek süresi bitmiş Go sürümleri kullanılmadıkça eski anlambilimle derlenemeyecek
Go 1.21’de önizleme olarak denemek
- Go 1.21, döngü kapsamı değişikliğinin önizleme sürümünü içeriyor
GOEXPERIMENT=loopvar ayarıyla derleme yapıldığında go.mod içindeki go satırı yok sayılıyor ve tüm döngülere yeni anlambilim uygulanıyor
- Bir paketin ve tüm bağımlılıklarının yeni döngü anlambilimi altında testleri geçip geçmediğini kontrol etmek için şu şekilde çalıştırılabilir:
GOEXPERIMENT=loopvar go test
- Go Playground’da programın en üstüne
// GOEXPERIMENT=loopvar yorumu eklenerek yeni anlambilim denenebilir
- Google’ın dahili Go araç zinciri, 2023 Mayıs başından itibaren tüm derlemelerde bu modu zorunlu kılacak şekilde yamanmıştı ve sonraki 4 ay boyunca üretim kodunda bir sorun raporlanmadı
Yeni anlambilimin ortaya çıkardığı test hataları
- Yeni döngü anlambilimi üretim kodunda sorun yaratmadı, ancak yanlış şekilde geçmekte olan testleri ortaya çıkardı
t.Parallel kullanan alt test örneğinde Go 1.21, tüm döngü bitene kadar her alt testi bekletip ardından paralel çalıştırıyor
- Döngü bittiğinde
v her zaman 6 olduğundan tüm alt testler 6’nın çift olduğunu kontrol edip başarılı oluyor
- Oysa gerçek test vakaları arasında
1 de bulunduğu için testin başarısız olması gerekir
- Go 1.21’de
loopclosure çözümleyicisinin hassasiyeti artırıldı; bu sayede bu sorun belirlenip raporlanabiliyor
- Go Playground rapor örneği: program örneği
go vet testlerde bu tür sorunları raporluyorsa, bunları düzeltmek Go 1.22’ye hazırlık açısından faydalı olur
- Yeni anlambilim uygulandığında belirli test başarısızlıklarına neden olan döngüleri bulmak için araçlar ve örnekler SSS içinde özetlenmiştir
Daha fazla okuma
1 yorum
Hacker News yorumları
Çok daha eski örnekler de olabilir ama 60 saniyelik bir aramayla bu davranış hakkında bulduğum en eski uyarı, 30 yılı aşkın süre önce, 1992’de yayımlanan comp.lang.lisp FAQ idi.
DOTIMES,DOLIST,DOyineleme değişkenini güncellerken bağlama değil atama kullandığından, örnekte olduğu gibilambdan’yi yakalarsa 10 closure’ın tamamının aynıNdeğişkeninin değeri üzerine oluşturulduğu açıklanıyor.Referansla yakalıyorsanız aslında beklenen davranış bu.
Yine de çalışma biçimini bir kez öğrenince sorun olmaktan çıkıyor; gerekirse formu seçip makro açılımına bakarak nasıl uygulandığını kontrol edebilirsiniz.
C# dil ekibi de C# 4.0’da hafif closure’ları getirdikten sonra aynı sorunla karşılaştı ve bunun kısa sürede bir tuzak olduğu ortaya çıktı.
Kullanıcılar neredeyse her zaman döngü değişkenini yanlış kullanıyordu ve C# 5.0’da geriye dönük uyumluluğu bozan bir değişiklik yapıldı.
Eric Lippert bu açıdan “neden”i iyi açıklayan bir yazı yazmıştı: https://ericlippert.com/2009/11/12/closing-over-the-loop-var...
Asıl C# 5 duyuru yazısını bulmak zordu; umarım 2012’den sonra Microsoft alan adındaki çeşitli blog taşımaları sırasında kaybolmamıştır.
Python 2’den 3’e geçerken yalnızca string türü değişikliğinin bile nasıl bir fırtına kopardığını düşünürsek, bu değişikliğin Python 4.0’dan önce geleceğini sanmıyorum.
Üstelik birileri Python’ın böyle şeyleri düzeltmediği için kötü olduğunu söyleyip, sonra da 2003’te yazdığı script çalışmıyor diye yine Python’a sövecektir.
Dil değişikliği önerilerinin varsayılan olarak sahip olması gereken “önce reddet” eşiğini aşmasında büyük rol oynadığını düşünüyorum.
Beni ayrıca ciddi biçimde ikna eden bir başka nokta da açık kaynak kod tabanlarını tarayıp düzeltilen hatalarla yeni oluşan hatalar arasındaki dengeye bakmalarıydı.
Değerle aktarım olduğu için çağrı anındaki değişken durumunu yakalar ve koddaki belirsizliği azaltır.
Değişkenleri garip biçimde yakalamaya çalışırsanız, örneğin bir diziyi map’e dönüştürmek için biriktirdiğiniz koleksiyon ile tanımlanmış değişkenler birbirinden farklı davranmaya başlar.
Go, bu davranışı yalnızca döngü sayacına uygulayarak denge kurmaya çalışıyor gibi görünüyor ama yine de bazı değişkenler garip davranıyor.
Özellikle girdiyi doğrudan taramak için birden fazla döngü değişkeni tanımlanan durumlarda ne olacağını merak ediyorum.
for(let)döngüsünü getirdi.https://eli.thegreenplace.net/2019/go-internals-capturing-lo... bu sorunu daha ayrıntılı açıklıyor gibi görünüyor.
i := inumarasının düşündüğümden tamamen farklı bir nedenle çalışması ilginç.Başta yeni
igoroutine’e geçirildiği için kaçış analizinin bunu leksik kapsamın dışına kaçıyor diye işaretlediğini, bu yüzden heap’te ayrıldığını ve her yinelemede bir heap ayırması oluşarak her goroutine’in kendine özgü bir bellek konumuna başvurduğunu sanmıştım.Gerçekte ise Go derleyicisinin referansla yakalama ile değerle yakalama arasında seçim yapan bir sezgisel yöntemi var ve başlatmadan sonra güncellenmeyen değerleri değer olarak yakalama koşulu bulunuyor.
Yeni
i,forgövdesinin kapsamında ve döngünün kendisi tarafından güncellenmediği için başlatmadan sonra güncellenmeyen bir değer olarak değerlendiriliyor; böylece heap ayırması olmadan değerle yakalayan kod üretiliyor.İkincisinin daha iyi olduğunu anlıyorum ama ilk yöntemin neden bununla birlikte gerçekleşmediğini Go’yu derinlemesine bilen birinden duymak isterim.
Bu değişiklik, mevcut davranışa dayanan programları bozmaz mı?
go.modiçindego 1.22veya daha üstünü bildiren modüllerdeki paketlere uygulanırDosya bazında ise
//go:buildsatırı kullanılarak da belirlenebilirBu vaat, Go 1 spesifikasyonuyla yazılmış programların spesifikasyonun ömrü boyunca değişiklik gerektirmeden derlenmeye ve doğru çalışmaya devam etmesi gerektiğini; bir gün Go 2 spesifikasyonu çıkabilecek olsa da o zamana kadar Go 1.1, Go 1.2 gibi nokta sürümlerde de bugün çalışan Go programlarının çalışmaya devam etmesi gerektiğini söyler
Bu tasarım yüzünden istemeden hata üreten kişi sayısının, düzeltmeden etkilenen kişi sayısından çok daha fazla olacağını düşündüler
Hatırladığım kadarıyla Google kod tabanında veya GitHub kodlarında bu değişikliğin beklenen davranışı bozduğu durumlar neredeyse hiç yoktu
Etkilenen kod tabanlarının ne kadar az olduğunu doğrulayıp, yeni davranışı kullanmak için
go.modiçindeki sürüm belirtimiyle kodun aktif olarak değiştirilmesini gerektiren bir mekanizma oluşturduktan sonra geriye dönük uyumluluğu bozmaya karar verdilerhttps://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
Çoğundan öneri belgesinde hiç bahsedilmemişti
Python’da da bu sorunu yaşamıştım ama yakın zamanda değil
Python mı değişti, yoksa ben mi sorunu fark etmeye başladım, emin değilim
Python’da hâlâ sorun olabileceğini yalnızca şu kod bile yeterince gösteriyor:
funcs = [(lambda: x) for x in range(3)];funcs[0]()çıktısı2olurPython eskiden daha kötüydü; liste üreteçlerinin dışındaki kapsamı bile paylaşıyordu
Liste üretecinde veya döngü içinde lambda kullandığınızda
x’in o anki değerini değil,xdeğişkenine olan referansı yakalarfuncs[0]()çağrıldığındaxzatenrange’in son değeri olan2olarak ayarlanmıştırİstediğiniz davranışı elde etmek için
x’i lambda’nın varsayılan argümanı olarak geçirebilirsiniz:funcs = [(lambda x=x: x) for x in range(3)]Go’yu sadece biraz kullandım ve bu değişikliğin çözdüğü genel problemi biliyorum, ama letsencrypt örneği ya da
"range c.informerMap"ile"range alarms"arasındaki daha incelikli örneği pek anlayamıyorumfor k, v := range someMapiçindevmap değer türü mü ve tüm döngü boyunca tek bir bağlama olup her yinelemede kopyalanıyor mu? Öyleyse sorun açıklanıyor, amav’nin map’in içini gösteren bir referans olmasını beklerdimSpesifikasyondaki “For statements with range clause” bölümüne hızlıca göz attığımda yanıtı bulamadım; Go’ya pek dokunmadığım için muhtemelen yanlış yere baktım: https://go.dev/ref/spec#For_statements
Düzenleme: Yanıt kod bloğu biçimindeki tablodaymış. Banner gibi görüp geçmişim sanırım.
v’nin referans değil, kopyalanmış değer olması şaşırtıcıDizi yuvalarına pointer destekler, ama
for rangeher yuvayı gösteren pointer vermek yerine kopyalarv’nin türüintolurDeğerdir;
int’e pointer değildirMerak ediyorsanız bakabilirsiniz: https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
Temel olarak derleyici, otomatik dereference nedeniyle
go a.Monitor(b)ifadesini(&a).Monitor(b)biçimine çeviriyor“İleriye dönük uyumluluk çalışmalarının sonucu olarak Go 1.21,
go 1.22veya üzerini bildiren kodu derlemeye çalışmaz. Go 1.20.8 ve Go 1.19.13 nokta sürümlerine de aynı etkiye sahip özel bir işlem koyduk; dolayısıyla Go 1.22 yayımlandığında, yeni semantiğe dayanarak yazılmış kod, çok eski ve desteklenmeyen bir Go sürümü kullanılmadığı sürece asla eski semantiklerle derlenmeyecek” kısmının nasıl çalıştığını merak ediyorumBir paket 1.22’ye sabitlenmişse ve ben 1.18 ile derlersem, derlenir mi yoksa 1.22 derleyicisi gerektiğine dair hata mı verir?
Go 1.21’de
go.moddosyasının sürüm numarası biçimini değiştirdikleri için Go 1.18 ile build etmeye çalışırsango.mod:3: invalid go version '1.21.0': must match format 1.23gibi bir hata çıkıyorAncak bu yalnızca
go mod initile modül oluşturulduğunda böyle;go.modiçine ellego 1.21yazarsan itiraz etmeden build ediyorOldukça hoş bir özellik ama şaşırtıcı bir davranış ve ikili dosya almak için Google’ın kontrol ettiği bir sunucuya bağlanması nedeniyle biraz tereddüt ettiriyor
Modül proxy’siyle birlikte Go’daki en ikircikli hissettiren özelliklerden biri; Go, Google’ın yalnızca pay sahibi olduğu bir vakıf tarafından yönetilseydi çok daha rahat hissederdim
Düzenleme: Düşününce bu, bir bağımlılık başka bir sürüm bildirdiğinde değil, mevcut modül bildirdiğinde geçerli; bu yüzden asıl sorudan farklı
Bu yüzden Go 1.18 kullanmak aktif olarak tehlikeli hale geliyor
Go 1.19’da derleyici hatası vermesi gerekir
Zaten Go eski sürümlere ve standart kütüphaneye güvenlik hatası düzeltmeleri yapmadığı için, böyle sürümleri kullanmanın başlı başına riskli olduğunu düşünüyorum
Ama Go 1.22 ile derlesen bile kodun hâlâ Go 1.18 semantiklerine sahip olur
Go bazı açılardan gerçekten tuhaf bir dil
Çok keskin görüşleri olan bir dil olmasına rağmen aynı zamanda fazla görüşsüz bir dil gibi görünüyor
c.informerMapüzerinde dönen kodlaalarmsüzerinde dönen kod arasındaki farkın ne olduğundan emin değilim ama tahminime göre bir taraftaki döngü değişkeni pointer, diğer taraftaki değer olabilirMetot çağrısı pointer receiver kullandığı için, değer olduğunda derleyici receiver’a otomatik olarak referans ekliyor olabilir mi?
https://github.com/adobe/kratos/blob/93246f92d53feba73743dbf...
https://github.com/StalkR/goircbot/blob/6081ed5d1d74f01767d7...
Fark şu: birinde
informerbir interface olduğu için metot çağrısı hemeninformer.Runolarak çözümleniyor ve sorun olmuyorDiğerinde
a,Alarmstruct’ı ve değer olarak kopyalanıyor;Monitormetodu ise pointer receiver alıyorBu yüzden derleyici fiilen
go a.Monitor(b)ifadesinigo (&a).Monitor(b)biçimine dönüştürüyor; bu da döngü değişkenine referans oluşturup soruna yol açıyorİkincisinde ise
asonundaalarms’ın son elemanının değerini taşıdığı için yazıda açıklanan asıl sorunun ortaya çıktığını tahmin ediyorumİç bilgim bu kadar, ama slice’ların heap’te backing array’i olduğu için pointer’lar veya referanslar bir ölçüde işin içine giriyor
Bunu okuyunca ciddi şekilde rahatladım
Go’daki en büyük kusurlardan biri düzeltiliyor
foo, err := getFoo(); if err != nil ...sonrasındabar, err := getBar(); fmt.Println(bar)gibi yazarsangetBar’ın hata kontrolünü kaçırırsınKapsam kuralları yüzünden
if foo, err := getFoo(); err != nilkalıbı, iç içe geçme biraz derinleştiğinde başa çıkılmaz hale geliyorAyrıca geçersiz durumlar yaratıyor.
getFoohata döndürdüğünde ne döndürmeli? API’yi pointer döndürecek şekilde değiştiripnilmi döndürmeli, yoksa geçersiz durumda kısmen oluşturulmuş bir nesne mi bırakmalı diye düşünmek zorunda kalıyorsun