Go dilinde defer, bir fonksiyon çağrısını içinde bulunduğu fonksiyon dönene kadar erteler; ertelenen çağrılar ters sırayla (son giren ilk çıkar, LIFO) çalışır ve argümanları defer satırı çalıştığı anda değerlendirilir. panic, normal akışı durdurup yığını geri sarmaya başlatan yerleşik fonksiyondur. recover ise yalnızca ertelenmiş bir fonksiyonun içinden doğrudan çağrıldığında işe yarar: panik değerini yakalar ve programın normal şekilde devam etmesini sağlar. Go'da beklenen hatalar error dönüş değeriyle bildirilir; panic gerçekten devam edilemeyen durumlar içindir.
Bu yazıda defer'in üç temel kuralını, isimli sonuç değerlerini değiştirmeyi, panik sırasında ertelenmiş çağrıların nasıl çalıştığını, recover'ın sınırlarını, çalışma zamanı hatalarını error değerine çevirmeyi, döngü içinde defer tuzağını ve os.Exit ile ilişkisini Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Hata değerleriyle çalışmanın genel yolu için Go error ve hata yönetimi yazısına, serinin tamamı için Go öğrenme rehberi sayfasına bakabilirsiniz.
defer neden var?

Go Blog'daki "Defer, Panic, and Recover" yazısı iki dosya açan bir kopyalama fonksiyonuyla başlar: ikinci dosya açılamadığında fonksiyon erken döner ve ilk dosyayı kapatmayı unutur. Her dönüş noktasına kapatma kodu eklemek mümkündür ama fonksiyon büyüdükçe unutmak kolaylaşır. defer ile kaynağı açtığınız satırın hemen altına kapatma çağrısını yazarsınız ve fonksiyon hangi yoldan dönerse dönsün çağrı çalışır.
Effective Go bu yaklaşımın iki avantajını sayar: fonksiyona sonradan yeni bir dönüş yolu eklendiğinde kapatmayı unutmak mümkün olmaz ve kapatma kodu açma kodunun yanında durduğu için okunabilirlik artar. En bilinen kullanımlar dosya kapatma (defer f.Close()), kilit bırakma (mu.Lock() ardından defer mu.Unlock()) ve izleme (trace) çıktılarıdır. Effective Go ayrıca defer'in blok tabanlı değil fonksiyon tabanlı olduğunu vurgular: ertelenen çağrı, bulunduğu süslü parantez bloğu bittiğinde değil, fonksiyon döndüğünde çalışır.
Kural 1 ve 2: argümanlar hemen, çağrılar ters sırayla
Spesifikasyona göre bir defer deyimi her çalıştığında fonksiyon değeri ve çağrının parametreleri her zamanki gibi değerlendirilip saklanır, ama fonksiyon çağrılmaz. Ertelenen fonksiyonlar, çevreleyen fonksiyon dönmeden hemen önce, ertelendikleri sıranın tersiyle çağrılır. Bu iki kural birlikte şu sonucu verir:
package main
import "fmt"
func main() {
fmt.Println("başla")
for i := range 3 {
defer fmt.Println("defer", i) // LIFO: son eklenen ilk çalışır
}
x := 1
defer fmt.Println("argüman:", x) // x şimdi değerlendirilir: 1
defer func() {
fmt.Println("closure:", x) // x dönüş anında okunur: 2
}()
x = 2
fmt.Println("bitti")
}
// Çıktı:
// başla
// bitti
// closure: 2
// argüman: 1
// defer 2
// defer 1
// defer 0Döngüdeki üç defer sırasıyla 0, 1, 2 değerleriyle saklandı ve ters sırada çalıştı. defer fmt.Println("argüman:", x) satırında x o anda değerlendirildiği için 1 yazıldı, sonradan yapılan x = 2 ataması bunu etkilemedi. Kapanış (closure) olarak ertelenen fonksiyon ise argüman almıyor; x değişkenini fonksiyon döndüğü anda okuduğu için 2 yazdı. Bu fark pratikte önemlidir: döngü değişkenini ya da sonradan değişecek bir değeri yakalamak istiyorsanız argüman olarak geçirin; son değeri görmek istiyorsanız kapanış kullanın.
Spesifikasyon iki ayrıntı daha belirtir. Birincisi, defer sonrasındaki ifade bir fonksiyon ya da metot çağrısı olmalıdır ve parantez içine alınamaz. İkincisi, ertelenen fonksiyon değeri nil ise panik defer deyimi çalıştığında değil, fonksiyon çağrılmaya çalışıldığında oluşur. Effective Go'ya göre metot ertelendiğinde alıcı da argümanlarla birlikte o anda değerlendirilir.
Kural 3: isimli sonuç değerlerini değiştirmek
Fonksiyon açık bir return ile dönüyorsa, ertelenen fonksiyonlar return deyiminin sonuç parametrelerini atamasından sonra ama fonksiyon çağırana dönmeden önce çalışır. Sonuç parametreleri isimliyse ve ertelenen kapanış onları görebiliyorsa, dönüş değerini değiştirebilir:
package main
import "fmt"
func sayi() (sonuc int) {
defer func() {
sonuc *= 7 // return 6 atamasından sonra çalışır
}()
return 6
}
func isimsiz() int {
x := 6
defer func() {
x *= 7 // dönüş değerini etkilemez
}()
return x
}
func iz(ad string) string {
fmt.Println("giriş:", ad)
return ad
}
func cikis(ad string) { fmt.Println("çıkış:", ad) }
func islem() {
defer cikis(iz("islem")) // iz hemen, cikis en sonda çalışır
fmt.Println("islem gövdesi")
}
func main() {
fmt.Println(sayi(), isimsiz())
islem()
}
// Çıktı:
// 42 6
// giriş: islem
// islem gövdesi
// çıkış: islemsayi fonksiyonunda return 6 önce sonuc değişkenine 6 atar, sonra ertelenen kapanış onu 7 ile çarpar ve çağırana 42 döner. Bu örnek doğrudan spesifikasyondaki örneğin uyarlamasıdır. isimsiz fonksiyonunda ise dönüş değeri isimsiz olduğu için return x değeri ayrı bir sonuç yuvasına kopyalanır; kapanışın x üzerinde yaptığı değişiklik dönüşü etkilemez. Ertelenen fonksiyonun kendi dönüş değerleri varsa spesifikasyona göre bunlar atılır.
Programın ikinci kısmı Effective Go'daki izleme kalıbının bir uyarlamasıdır: defer cikis(iz("islem")) satırında iz çağrısı bir argüman ifadesi olduğu için hemen çalışır ve "giriş" yazar; cikis ise fonksiyon dönerken çalışır. Tek satırla giriş ve çıkış kaydı tutmanın pratik bir yoludur.
Hatayı kaybetmemek: defer içinde Close
defer f.Close() yazımı Close metodunun döndürdüğü hatayı yok sayar. Okuma için açılan dosyalarda bu genellikle sorun değildir, ama yazma işlemlerinde kapatma hatası verinin diske yazılamadığını gösterebilir. İsimli err sonucu ile bu hatayı çağırana iletebilirsiniz:
package main
import (
"errors"
"fmt"
)
type Dosya struct{ bozuk bool }
func (d *Dosya) Close() error {
if d.bozuk {
return errors.New("kapatma hatası")
}
return nil
}
func yaz(d *Dosya) (err error) {
defer func() {
if kerr := d.Close(); kerr != nil && err == nil {
err = kerr // Close hatasını kaybetmeyin
}
}()
return nil
}
func main() {
fmt.Println(yaz(&Dosya{}))
fmt.Println(yaz(&Dosya{bozuk: true}))
}
// Çıktı:
// <nil>
// kapatma hatasıKapanış, fonksiyonun asıl hatası yoksa (err == nil) Close hatasını sonuca yazar; asıl hata varsa onu ezmez. Birden fazla hatayı birlikte döndürmek isterseniz errors.Join kullanabilirsiniz; ayrıntılar Go error ve hata yönetimi yazısında. Dosya işlemlerinin kendisi için Go dosya okuma ve yazma yazısına bakın.
panic: yığını geri sarmak

Spesifikasyona göre bir F fonksiyonunda açık bir panic çağrısı ya da bir çalışma zamanı paniği F fonksiyonunun çalışmasını sonlandırır. F tarafından ertelenmiş fonksiyonlar her zamanki gibi çalışır, ardından F fonksiyonunu çağıranın ertelediği fonksiyonlar çalışır ve bu, çalışan goroutine'deki en üst fonksiyona kadar devam eder. Bu noktada program sonlandırılır ve panic argümanının değeri dahil hata raporlanır. Bu sürecin adı panicking'dir.
Panik yalnızca açık panic çağrısıyla oluşmaz; dizi sınırı dışına erişim, nil map'e yazma, nil işaretçi dolaylama, sıfıra bölme ve başarısız tip doğrulaması gibi çalışma zamanı hataları da panik üretir. Panik sırasında bile ertelenmiş çağrılar çalışır:
package main
import "fmt"
func main() {
defer fmt.Println("defer çalıştı")
var s []int
fmt.Println(s[3])
}defer çalıştı
panic: runtime error: index out of range [3] with length 0recover: paniği yakalamak
recover paniklemekte olan bir goroutine'in kontrolünü geri almayı sağlar. Effective Go'ya göre geri sarma sırasında çalışan tek kod ertelenmiş fonksiyonlar olduğu için recover yalnızca onların içinde işe yarar. Go Blog'daki klasik örneğin uyarlaması, panik anında hangi ertelenmiş çağrıların hangi sırada çalıştığını gösteriyor:
package main
import "fmt"
func main() {
f()
fmt.Println("f normal şekilde döndü.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("f içinde kurtarıldı:", r)
}
}()
fmt.Println("g çağrılıyor.")
g(0)
fmt.Println("g normal şekilde döndü.") // çalışmaz
}
func g(i int) {
if i > 2 {
fmt.Println("Panik!")
panic(fmt.Sprintf("i=%v", i))
}
defer fmt.Println("g içinde defer", i)
fmt.Println("g yazdırıyor", i)
g(i + 1)
}
// Çıktı:
// g çağrılıyor.
// g yazdırıyor 0
// g yazdırıyor 1
// g yazdırıyor 2
// Panik!
// g içinde defer 2
// g içinde defer 1
// g içinde defer 0
// f içinde kurtarıldı: i=3
// f normal şekilde döndü.g fonksiyonu kendini özyinelemeli çağırır ve i 3 olduğunda panikler. Geri sarma sırasında her g çerçevesinin ertelediği Println çağrıları içten dışa doğru çalışır. Sonunda f içindeki ertelenmiş kapanış recover çağırır, panik değerini ("i=3") alır ve panik durur. Spesifikasyona göre bu durumda g ile panik noktası arasındaki fonksiyonların durumu atılır ve f çağırana normal şekilde döner; bu yüzden "g normal şekilde döndü." satırı hiç yazılmaz ama main devam eder. f içindeki kapanış kaldırılsaydı panik main fonksiyonuna kadar ulaşır ve program çökerdi.
Spesifikasyon recover dönüş değerinin iki durumda nil olduğunu söyler: goroutine paniklemiyorsa ya da recover ertelenmiş bir fonksiyon tarafından doğrudan çağrılmamışsa. İkinci durum sık gözden kaçar:
package main
import "fmt"
func yardimci() {
fmt.Println("yardımcı içinde recover:", recover()) // doğrudan defer edilen fonksiyon değil
}
func dene() {
defer func() {
yardimci()
}()
panic("hata")
}
func main() {
defer func() {
fmt.Println("main kurtardı:", recover())
}()
dene()
}
// Çıktı:
// yardımcı içinde recover: <nil>
// main kurtardı: hatayardimci fonksiyonu ertelenmiş kapanışın içinden çağrılıyor, yani recover ertelenmiş fonksiyon tarafından doğrudan çağrılmıyor; sonuç nil olur ve panik devam eder. Panik bir üst çerçevedeki main içindeki ertelenmiş kapanışa ulaşır ve orada yakalanır. Effective Go bu kuralın bir faydasını da belirtir: ertelenmiş kod, kendi içinde panic ve recover kullanan kütüphane fonksiyonlarını çağırabilir ve bu çağrılar paniklemekte olan durumdan etkilenmez.
Çalışma zamanı hatasını error değerine çevirmek
Effective Go, bir paketin içeride panik kullanıp dışarıya error döndürmesini makul bir kalıp olarak gösterir; Go Blog da standart kütüphanedeki encoding/json paketinin bu yöntemi kullandığını ve kütüphanelerin genel kuralının, içeride panik kullanılsa bile dış API'de açık hata değerleri sunmak olduğunu belirtir. Önemli nokta, yalnızca beklediğiniz panikleri yakalamak ve diğerlerini yeniden fırlatmaktır:
package main
import (
"errors"
"fmt"
"runtime"
)
func bol(a, b int) (sonuc int, err error) {
defer func() {
if r := recover(); r != nil {
if re, ok := r.(runtime.Error); ok {
err = fmt.Errorf("bölme başarısız: %w", re)
return
}
panic(r) // beklenmeyen panik: yeniden fırlat
}
}()
return a / b, nil
}
func main() {
fmt.Println(bol(10, 2))
_, err := bol(1, 0)
fmt.Println(err)
var re runtime.Error
fmt.Println(errors.As(err, &re))
fmt.Println(recover()) // panik yokken nil
}
// Çıktı:
// 5 <nil>
// bölme başarısız: runtime error: integer divide by zero
// true
// <nil>Sıfıra bölme bir çalışma zamanı paniğidir ve panik değeri runtime.Error arayüzünü uygular. Ertelenmiş kapanış değeri bu arayüze göre denetler; uyuyorsa isimli err sonucuna %w ile sarmalanmış bir hata yazar, uymuyorsa paniği panic(r) ile yeniden başlatır. Effective Go bu "yeniden panik" yaklaşımının beklenmedik hataları gizlemediğini, çünkü hem asıl hem yeni hatanın çökme raporunda görüneceğini belirtir. main içindeki son satır, panik yokken recover çağrısının nil döndürdüğünü gösteriyor. Sarmalanmış hataları errors.As ile denetleme ayrıntısı Go error, tip doğrulama ise Go type assertion ve type switch yazısında.
Effective Go'daki bir başka kullanım, bir sunucuda başarısız olan tek bir goroutine'i diğerlerini etkilemeden kapatmaktır: her iş bir safelyDo benzeri fonksiyonda, recover çağıran bir defer ile çalıştırılır. Panik goroutine sınırını aşmaz: bir goroutine'deki recover başka bir goroutine'deki paniği yakalayamaz, bu yüzden koruma her goroutine'in kendi içinde kurulmalıdır. Goroutine'ler için Go goroutine yazısına bakın.
panic(nil) ve PanicNilError
Go 1.21 sürüm notlarına göre, paniklemekte olan bir goroutine'de doğrudan ertelenmiş fonksiyondan çağrılan recover dönüş değerinin nil olmayacağı artık garanti edilir. Bunu sağlamak için panic fonksiyonunu nil bir arayüz değeriyle (ya da tipsiz nil ile) çağırmak *runtime.PanicNilError tipinde bir çalışma zamanı paniğine neden olur:
package main
import "fmt"
func main() {
defer func() {
r := recover()
fmt.Printf("%T: %v\n", r, r)
}()
panic(nil)
}
// Çıktı:
// *runtime.PanicNilError: runtime error: panic called with nil argumentSürüm notları, eski davranışın GODEBUG=panicnil=1 ayarıyla geri getirilebileceğini ve go.mod dosyasında go 1.20 veya daha eski sürüm bildiren modüllerde bu ayarın otomatik etkin olduğunu belirtir. Kendi kodunuzda panic argümanı olarak her zaman anlamlı bir değer (bir hata ya da açıklayıcı bir string) kullanın.
Döngü içinde defer tuzağı
defer fonksiyon tabanlı olduğu için, bir döngünün içinde ertelenen çağrılar her yinelemenin sonunda değil, fonksiyonun sonunda çalışır. Çok sayıda dosya açan bir döngüde bu, tüm dosyaların fonksiyon bitene kadar açık kalması anlamına gelir. Çözüm, döngü gövdesini ayrı bir fonksiyona (ya da hemen çağrılan bir fonksiyon literaline) taşımaktır:
package main
import "fmt"
type Kaynak struct{ ad string }
func ac(ad string) *Kaynak {
fmt.Println("açıldı:", ad)
return &Kaynak{ad}
}
func (k *Kaynak) Kapat() { fmt.Println("kapandı:", k.ad) }
func hepsiSonda(adlar []string) {
for _, ad := range adlar {
k := ac(ad)
defer k.Kapat() // fonksiyon bitene kadar bekler
}
fmt.Println("döngü bitti")
}
func tekTek(adlar []string) {
for _, ad := range adlar {
func() {
k := ac(ad)
defer k.Kapat() // iç fonksiyon bitince çalışır
}()
}
fmt.Println("döngü bitti")
}
func main() {
hepsiSonda([]string{"a", "b"})
fmt.Println("---")
tekTek([]string{"a", "b"})
}
// Çıktı:
// açıldı: a
// açıldı: b
// döngü bitti
// kapandı: b
// kapandı: a
// ---
// açıldı: a
// kapandı: a
// açıldı: b
// kapandı: b
// döngü bittihepsiSonda fonksiyonunda iki kaynak da döngü bittikten sonra ve ters sırayla kapandı. tekTek fonksiyonunda ise her yineleme kendi fonksiyon çağrısı içinde çalıştığı için kaynak, bir sonraki açılmadan önce kapandı. Uzun süren döngülerde, örneğin binlerce dosya ya da veritabanı satırı işlerken, ikinci yaklaşım kaynak sızıntısını önler.
os.Exit ertelenmiş çağrıları çalıştırmaz
os paketinin belgesine göre os.Exit programı hemen sonlandırır ve ertelenmiş fonksiyonlar çalışmaz. log.Fatal ailesi de çıktıyı yazdıktan sonra os.Exit çağırdığı için aynı sonucu doğurur. Temizlik kodu defer içinde olan bir programda main içinden doğrudan os.Exit çağırmak bu temizliği atlar:
package main
import (
"fmt"
"os"
)
func main() {
defer fmt.Println("bu satır yazılmaz")
fmt.Println("çıkılıyor")
os.Exit(3)
}çıkılıyor
exit status 3"exit status 3" satırını program değil go run komutu yazar. Yaygın çözüm, asıl işi hata döndüren bir run() fonksiyonuna taşımak ve main içinde yalnızca if err := run(); err != nil { os.Exit(1) } yazmaktır; böylece run içindeki tüm defer çağrıları çıkıştan önce çalışır. Komut satırı programlarının yapısı için Go flag ve Go os/exec yazılarına bakabilirsiniz.
defer, panic ve recover özeti
| Konu | Davranış | Kaynak |
|---|---|---|
| Argüman değerlendirme | defer satırı çalıştığında | Spesifikasyon, Go Blog |
| Çalışma sırası | Ters sırayla (LIFO), fonksiyon dönerken | Spesifikasyon |
| İsimli sonuç | return ataması sonrası değiştirilebilir | Spesifikasyon |
| Panik sırasında | Ertelenmiş çağrılar yine çalışır | Spesifikasyon |
| recover dönüşü | Panik yoksa veya dolaylı çağrıda nil | Spesifikasyon |
| panic(nil) | *runtime.PanicNilError paniği | Go 1.21 sürüm notları |
| os.Exit | Ertelenmiş çağrılar çalışmaz | os paketi |
| Kapsam | Blok değil fonksiyon | Effective Go |
Sık yapılan hatalar

- Döngüde defer ile kaynak kapatmak: kaynaklar fonksiyon sonuna kadar açık kalır; gövdeyi ayrı fonksiyona taşıyın.
- Argümanın sonradan değişeceğini sanmak:
defer fmt.Println(x)o andakixdeğerini kullanır. - recover'ı yardımcı fonksiyonda çağırmak: recover yalnızca ertelenmiş fonksiyonun içinden doğrudan çağrıldığında paniği yakalar.
- Her paniği yutmak: beklenmeyen panikleri gizlemek hataları saklar; tip kontrolü yapıp diğerlerini yeniden fırlatın.
- Beklenen hatalar için panic kullanmak: dosya bulunamadı, geçersiz girdi gibi durumlar
errorile döndürülmelidir; Effective Go kütüphanelerin panikten kaçınmasını önerir. - Close hatasını yok saymak: yazma işlemlerinde kapatma hatasını isimli
errsonucu ile iletin. - os.Exit ile temizliği atlamak:
mainiçinde defer kullanıyorsanız doğrudanos.Exitveyalog.Fatalçağırmayın. - Başka goroutine'in paniğini yakalamayı beklemek: recover yalnızca kendi goroutine'indeki paniği yakalar.
Panik mesajlarının toplu listesi için Go sık karşılaşılan hatalar yazısına bakabilirsiniz.
Sık Sorulan Sorular
defer ne zaman çalışır?
Çevreleyen fonksiyon dönerken: açık bir return sonrası, fonksiyon gövdesinin sonuna ulaşıldığında ya da goroutine paniklediğinde. Açık return varsa sonuç değerleri atandıktan sonra, çağırana dönmeden önce çalışır.
Ne zaman panic kullanmalıyım?
Programın devam edemeyeceği, gerçekten beklenmeyen durumlarda. Effective Go, kütüphane fonksiyonlarının panikten kaçınmasını, sorun aşılabiliyorsa programın çalışmaya devam etmesini önerir; olası bir istisna, kütüphanenin kendini hiç başlatamadığı başlatma (init) aşamasıdır. Beklenen hata durumları error ile döndürülür.
recover neden nil döndürüyor?
Ya o anda bir panik yoktur ya da recover ertelenmiş fonksiyonun içinden doğrudan değil, onun çağırdığı başka bir fonksiyondan çağrılmıştır. Ayrıca recover farklı bir goroutine'deki paniği yakalayamaz.
defer performansı etkiler mi?
Kaynak belgeler defer için bir maliyet rakamı vermez. Doğruluk ve okunabilirlik kazancı çoğu durumda belirleyicidir. Çok sık çağrılan bir döngü içinde ölçülebilir bir etki olduğundan şüpheleniyorsanız tahmin yürütmek yerine benchmark yazın; ayrıntı için Go benchmark ve fuzz testi yazısına bakın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Go öğrenme rehberi · Go error ve hata yönetimi · Go fonksiyonlar ve çoklu dönüş · Go dosya okuma ve yazma · Go sync: Mutex ve WaitGroup · Go sık karşılaşılan hatalar