Sorunun tarihi
defer ifadesi, Go'nun ilk sürümünden bu yana temel bir özellik olmuştur ve kaynak temizliğinin bir işlevden dönen yol hangisi olursa olsun çalıştığından emin olmak için tasarlanmıştır. Go'nun geliştirilmesinin başlarında, ekip, defer edilmiş işlevlerin adlandırılmış dönüş parametrelerini inceleme ve değiştirme yeteneğinin faydasını fark etti, özellikle de çıkışta kaydetme, hata sarmalama ve kaynak durumu doğrulama için. Bu yetenek, karmaşık kod kalıplarını gerektirmeden işlem geri alma hata raporlaması gibi kalıpları desteklemek için kasıtlı bir tasarım kararıydı.
Problem
(result int, err error) döndüren bir işlev düşünün. İşlev return 42, nil ifadesini yürüttüğünde, değerler adlandırılmış dönüş değişkenlerine result ve err atanır. Ancak eğer bir defer edilmiş işlev, bu atamadan sonra ama işlev gerçekten çağrıya dönmeden önce çalışıyorsa, çağrıcının ne alacağını değiştirebilir mi? Eğer dönüş değerleri isimlendirilmemişse (örneğin, func calculate() int), defer edilmiş işlevin dönüş slotuna erişimi yoktur. Dönüş değerlerinin ne zaman kesinleştiğini anlamak ve defer edilmiş kapatmaların bu değişkenleri nasıl yakaladığını tanımak belirsizlik yaratır.
Çözüm
Go, defer edilmiş işlevlerin adlandırılmış dönüş değerlerini değiştirmesine izin verir çünkü bu isimler, işlevin yığın çerçevesinde (veya kaçarken yığında) tahsis edilen yerel değişkenler gibi davranır. return ifadesi yürütüldüğünde, ifadeleri değerlendirir ve bunları adlandırılmış dönüş değişkenlerine atar. Sonrasında, Go defer edilmiş işlevleri LIFO (son giren ilk çıkar) sırasına göre yürütür. Eğer bir defer edilmiş işlev adlandırılmış bir dönüş değişkenine (örneğin, err) referans verirse, aynı bellek konumunda çalışır. Bu nedenle, defer edilmiş işlev içinde err'ye yapılan herhangi bir atama, return ifadesi tarafından ayarlanan değeri üzerini yazar. İsimlendirilmemiş dönüş değerleri bu erişilebilir konuma sahip olmadığından, defer edilmiş işlevler tarafından değiştirilemezler.
func example() (result int) { defer func() { result++ // Adlandırılmış dönüş değerini değiştirir }() return 10 // result 10'a ayarlanır, defer 11'e artırır }
Problem tanımı
Bir ödeme işleme hizmeti inşa ediyorduk, burada ProcessPayment işlevi fonları düşürür ve işlemi kaydederdi. İşlev (txnID string, err error) dönerdi. Kritik bir gereklilik ortaya çıktı: eğer veritabanı işlemi başarıyla tamamlanmış ancak sonraki denetim günlüğü yazma işlemi başarısız olmuşsa, hem işlem kimliğini (başarı) hem de denetim hatası bildiren bir hatayı döndürmemiz gerekti. Ancak, eğer ödeme düşürme işlemi kendisi başarısız olduysa, geri almalı ve o hatayı döndürmeliydik. Zorluk, işlevin en ciddi hatayı döndürmesini sağlarken, kısmi başarı durumunda işlem kimliğini korumaktı.
Farklı çözümler düşünüldü
Çözüm 1: Birden fazla dönüş ile hata toplama
İmza değiştirip ProcessPayment() (string, []error) olarak topluca hataları toplama düşüncesinde bulunduk. Bu yaklaşım tamamen şeffaflık sağladı ancak tek bir hata beklentisini ihlal etti. Her çağırıcının hata önceliklendirme mantığını uygulamak zorunda kalmasına neden oldu, bu da API yüzeyini önemli ölçüde karmaşıklaştırdı ve kodun bakımını zorlaştırdı.
Çözüm 2: Yapı tabanlı dönüş türü
Başka bir yaklaşım, TxnID, Err ve AuditErr alanlarını içeren bir PaymentResult yapısı oluşturmaktı. Bu, verileri kapsüllerken, çağırıcıların yapı alanlarını kontrol etmelerini gerektirdi, basit if err != nil kontrolleri yerine. Bu desen, sıklıkla çağrılan bir işlem için ağır hissettirdi ve standart Go'nun geleneklerinden sapmaya neden olarak kodun okunabilirliğini azalttı.
Çözüm 3: Defer ile adlandırılmış dönüş değeri manipülasyonu
Bir adlandırılmış dönüş değeri err error kullandık ve ana mantıktan sonra yürütülecek bir işlevi defer ettik. Bu defer edilmiş işlev, bir işlem kimliği oluşturulup oluşturulmadığını (başarıyla düşürüldüğünü gösterir) kontrol etti, ancak denetim günlüğü kaydı sırasında bir hata oluştu. O zaman mevcut hatayı denetim bağlamıyla sarmalayacak veya denetim hatasını ciddiyetine göre önceliklendirecekti. Bu, temiz (string, error) imzasını korurken, içsel karmaşık hata durumu yönetimine olanak sağladı.
Seçilen çözüm ve sonuç
Çözüm 3'ü seçtik. func ProcessPayment() (txnID string, err error) tanımlayarak ve err'ye referans veren bir kapanışı defer ederek, ana yürütme yolunun tamamlanmasının ardından nihai hatayı yakalayıp değiştirebildik. Eğer ödeme başarılı olduysa (txnID atandı) ancak denetim başarısız olduysa, defer edilmiş işlev err'yi denetim başarısızlığını yansıtacak şekilde güncelledi ve txnID'yi korudu. Bu yaklaşım API'yi geleneksel hale getirdi, hata dilimleri için tahsisatları önledi ve hata önceliklendirme mantığını işlev içinde merkezileştirdi. Sonuç olarak, çağrı noktalarında %40'lık bir kod tekrarı azaltımı ve hizmetin genelinde tutarlı hata işleme kalıpları elde ettik.
Defer edilmiş bir işlevin argümanlarının hemen değerlendirildiğini, neden adlandırılmış dönüşlerin değiştirilmesinin daha sonra olduğunu açıklayabilir misiniz?
Birçok aday defer edilmiş işlevin argümanlarının değerlendirilmesini defer edilmiş işlevin gövdesinin yürütülmesiyle karıştırır. defer fmt.Println(count) yazdığınızda, count hemen değerlendirilir ve saklanır. Ancak defer func() { result++ }() yazdığınızda, result yürütme zamanı gelene kadar değerlendirilmez; eğer result adlandırılmış bir dönüşse, döndürülecek olan aynı değişkene referans verir.
Yanıt:
Go'nun spesifikasyonu, defer edilmiş işlev çağrısının argümanlarının hemen değerlendirildiğini ancak işlevin çağrısının geciktiğini belirtir. Bir kapanış (func() { ... }) durumunda, defer edilen çağrının kendisine hiçbir argüman geçmez, bu nedenle defer edilen noktada hiçbir şey yakalanmaz. Bunun yerine, kapanış, değişkenleri referans yoluyla yakalar. Adlandırılmış dönüş değişkenleri, işlev prologunda bir kez tahsis edilir. return yürütüldüğünde, bu değişkenlere yazma yapılır. Ardından defer edilmiş kapanış yürütülür ve o aynı bellek adresini değiştirir. defer f(x) gibi kapanış olmayan deferlerde ise, x hemen geçici bir konuma kopyalanır, bu nedenle x daha sonra değişse bile, defer edilmiş çağrı orijinal değeri kullanır.
Panik ve geri almanın defer içinde değiştirilmiş adlandırılmış dönüşlerle nasıl etkileşime girdiğini açıklayabilir misiniz?
Adaylar genellikle geri alınmış bir panik durumunun adlandırılmış dönüşlerin değişikliklerini sürdürmesine izin verip vermediğini açıklamakta zorlanır.
Yanıt:
Bir panik meydana geldiğinde, Go yığını sararak defer edilmiş işlevleri yürütmeye başlar. Eğer defer edilmiş bir işlev recover() çağrısını gerçekleştirmişse, panik durur. Bu defer edilmiş işlev ayrıca bir adlandırılmış dönüş değerini de değiştirirse, değişiklik kalır çünkü adlandırılmış dönüş değişkeni, panik kurtarma süreci boyunca tahsis edilmiştir. Ancak, işlev normal bir şekilde (panik yok) geri dönüyorsa ama defer edilmiş bir işlev panik yapıyorsa, önceki defer edilmiş işlevler tarafından adlandırılmış dönüşlere yapılan tüm değişiklikler iptal edilir çünkü yeni panik normal dönüş yolunu değiştirir. Temel içgörü, recover'ün kontrolü çağrıya normal bir şekilde döndüğü için geri alma sırasında veya öncesinde adlandırılmış sonuçlara yapılan tüm değişikliklerin çağrıya görünür hale gelmesidir.
Defer değişikliğini sağlamak için yalnızca adlandırılmış dönüşlerin kullanılmasının performans üzerindeki etkisi nedir ve kaçış analizi yığın tahsisatını zorladığında?
Adaylar sık sık adlandırılmış dönüşlerin zaman zaman isimsiz dönüşlerden daha fazla yığın tahsisine sebep olabileceğini gözden kaçırır.
Yanıt: Adlandırılmış dönüş değerleri genellikle yerel değişkenler gibi davranır. Ancak bir defer edilmiş işlev bir adlandırılmış dönüşü (veya herhangi bir yerel değişkeni) referans alırsa, kaçış analizi, değişkenin ömrünün normal işlev yürütme çerçevesinin ötesine uzandığını belirler. Sonuç olarak, Go değişkeni yığından ziyade yığının üzerine tahsis eder. Bu tahsisat, çöp toplama baskısını artırır. Sıcak yolların içinde, adlandırılmış dönüşlerin kullanılmaması (defer değişikliğine ihtiyaç yoksa) tahsisatları azaltabilir. Derleyici basit durumları optimize eder, ancak tamamen adlandırılmış dönüşü referans yoluyla yakalayan defer kapanışları söz konusu olduğunda, yığın tahsisi kaçınılmazdır. Bu denge, mikro optimizasyonlar yerine doğru ve temiz API tasarımını tercih eder, eğer profil oluşturma bir darboğaz belirlemediyse.