Go dilindeki recover() işlevi, yalnızca panik nedeniyle meydana gelen geri sarma sürecinin bir parçası olarak çalışan bir deferred işlevinin içinde doğrudan çağrıldığında paniği durdurur. recover() bir yardımcı işlevin içinde çağrıldığında, bu yardımcı işlev bir deferred closure tarafından çağrıldığında çalışma zamanı, mevcut goroutine'in yürütme çerçevesinin aktif panikle ilişkili en üst düzey deferred çerçeve olmadığını tespit eder.
// Bu desen KURTARAMAZ: func handlePanic() { if r := recover(); r != nil { log.Println("Kurtarıldı:", r) } } func risky() { defer handlePanic() // recover() burada nil döner panic("hata") }
Çalışma zamanı, bu kontrolü g.recover alanı aracılığıyla sürdürür; bu alan, kurtarma yetkisine sahip deferred işlevinin yığın çerçeve işaretçisini saklar. recover() yürütüldüğünde, mevcut yığın işaretçisini bu saklanan değerle karşılaştırır; eğer eşleşmezse, recover() nil döner ve panik yığın boyunca yukarıya doğru yayılmaya devam eder. Bu mimari kısıtlama, kurtarma mantığının açık ve yerelleşmiş hale gelmesini sağlar, derinlemesine yerleşik yardımcı işlevlerin, yukarı düzey kurtarma işleyicilerine yayılması gereken panikleri yanlışlıkla yutmasını engeller.
Binlerce eşzamanlı goroutine işleyen yüksek verimli bir mikro hizmette, hatalı isteklerden kaynaklanan sunucu çökmelerini önlemek için merkezi bir panik kurtarma mekanizması uyguladık. İlk uygulama, logging ve metrikleri kapsayan bir yardımcı işlev olan SafeRecover()'ı kullanarak, geliştiricilerin her bir işleyici başlangıcında defer SafeRecover() kullanarak bu işlevi ertelemesini sağladı. Ancak, üretim sırasında bir istek işleyicisinde sıfıra bölme hatası ile ilgili bir olay sırasında hizmet çöktü; görünür kurtarma mekanizmasına rağmen panik yükseltilmedi ve bunun nedeni recover()'ın yardımcı içinde yer almasındandı, doğrudan çağrılmadı.
İlk olarak, geliştiricilerin her işlev giriş noktasında defer func() { if r := recover(); r != nil { ... } }() yazmalarını zorunlu kılmayı düşündük. Bu yaklaşım, çalışma zamanı uyumluluğunu sağlamak için recover()'a doğrudan erişim sağladı, ancak önemli ölçüde gereksiz kod ekledi ve insan tutarlılığına bağımlı hale getirdi, bu da büyük bir ekip için hata yapmaya açık hale geldi ve kod incelemeleri sırasında uygulanmasını zorlaştırdı.
İkinci yaklaşım, SafeRecover()'ı bir argüman olarak bir closure kabul edip o geçilen işlev içinde recover()'ı yürütmek ve ardından yardımcı mantığı çağırmak üzere değiştirmekti. Bu teknik olarak, recover()'ı deferred çerçeveye yerleştirerek gereksinimi karşılasa da, işleyicilerin kurtarma mantıklarını geri çağırmalarını gerektiren garip bir API oluşturdu; bu, kontrol akışını karmaşıklaştırdı ve okunabilirliği azalttı, gereksiz dolaylılık ekledi.
Sonunda, üçüncü yaklaşımı seçtik: HTTP yönlendirici düzeyinde bir middleware sarıcı uygulayarak defer func() { if r := recover(); r != nil { logAndMetrics(r) } }()'ı middleware'in deferred closure'ında doğrudan yürütmek. Bu çözüm, recover()'ın doğru yığın derinliğinde çağrılmasını sağladı ve endişelerin temiz ayrılmasını sürdürdü, böylece takip eden kaos testlerinde %100 panik yakalama oranı sağlandı ve bir sonraki çeyrek boyunca sıfır çökme döngüsü gerçekleşti.
Peki, recover() neden bir panik aktif olmasa bile deferred bir işlev dışındaki bir yerde çağrıldığında nil döner?
Deferred bir yürütme bağlamının dışındayken, recover() mevcut goroutine'in panik durumunu sorgular ve aktif bir panik kaydı bulamaz, bu nedenle hemen nil döner. İncelik, recover()'ın mevcut işlevin bir deferred yığın geri sarma sürecinin parçası olarak yürütülüp yürütülmediğini kontrol etmesidir, yalnızca programda bir panik olup olmadığını değil. Normal yürütme yollarından çağrıldığında, çalışma zamanı goroutine yapısındaki _panic alanının nil olduğunu bulur ve yan etkisiz olarak nil döner; bu, normal hata işleme sırasında yanlışlıkla geri kurtarma mekanizmalarının tetiklenmesini engeller.
Aynı goroutine'deki birden fazla deferred işlev recover()'ı çağırırsa ne olur ve neden sadece ilki başarılı olur?
Bir panik meydana geldiğinde, Go deferred işlevleri LIFO sırasına göre yürütür ve recover()'ı çağıran ilk deferred işlev atomik olarak goroutine'in içsel _panic bağlı listesinden aktif panik durumunu temizler. Sonraki deferred işlevler recover()'ı çağırdıklarında panik durumunun zaten çözüldüğünü bulurlar ve orijinal panik değerinin yerine nil alırlar. Bu tasarım, belirsiz panik işlemenin deterministik bir şekilde sağlanmasını garantiler; en içteki kurtarma kapsamı öncelik alır ve yığın normal yürütmeye döndüğünde hata yayılma mantığını karıştırabilecek fazla kurtarma girişimlerini engeller.
panic(nil) panic("nil") veya panic(0)'dan nasıl farklı davranır ve Go 1.21 bu davranışı neden değiştirdi?
Go 1.21'den önce, panic(nil) çağrısı yapıldığında çalışma zamanı, panik değerini özel bir işaretçi olarak ele alır ve recover() bunun yerine nil döner, bu da onu işlenmesi gereken bir panik bulamadığı bir recover() çağrısından ayırt edilemez hale getirir ve tehlikeli bir belirsizlik yaratır. Go 1.21 ve sonrası için, çalışma zamanı nil panik değerini otomatik olarak, "runtime error: panic called with nil argument" dizesini içeren bir hata durumuna dönüştürür; bu, recover()'ın her zaman bir panik müdahale ettiğinde, nil'den farklı bir değer döndürmesini sağlar. Bu değişiklik, hata işleme kodundaki belirsizliği ortadan kaldırarak, geliştiricilerin if r := recover(); r != nil ile güvenle kontrol etmelerini sağladı; böylece dönen nil gerçekten hiç panik olmadığını gösterir.