Go (Golang) — Context ve Hata Yönetimi: Çok Detaylı Bir Rehber
Bu doküman, idiomatik Go programlamanın en önemli — ve en çok yanlış anlaşılan — iki konusunu ele alıyor:
contextpaketi ve hata (error) yönetimi. Go’nun temel sözdizimini bilen ve bu iki alanda derinlemesine uzmanlaşmak isteyen geliştiriciler için yazılmıştır; iç işleyiş, kalıplar (pattern), tuzaklar ve gerçek dünya örnekleri dahil.
İçindekiler
- Bölüm I —
contextPaketi- 1. Context Neden Var?
- 2.
ContextArayüzü (Interface) - 3. Kök Context’ler:
context.Background()vecontext.TODO() - 4. İptal İşlemi:
context.WithCancel - 5. Timeout ve Deadline’lar
- 6. Değer Taşıma:
context.WithValue - 7. Context Yayılım (Propagation) Kuralları
- 8. Context Ağaçları ve İptal Nasıl Yayılır
- 9. Context ile İlgili Yaygın Tuzaklar
- 10. HTTP Sunucu ve İstemcilerde Context
- 11. Goroutine’ler ve
errgroupile Context - 12. Context İçin En İyi Uygulamalar Kontrol Listesi
- Bölüm II — Go’da Hata Yönetimi
- 1. Felsefe: Hatalar Birer Değerdir
- 2.
errorArayüzü - 3. Hata Oluşturma:
errors.Newvefmt.Errorf - 4. Sentinel (Nöbetçi) Hatalar
- 5. Özel Hata Tipleri
- 6. Hata Sarmalama (
%w) ve Açma (Unwrap) - 7.
errors.Isveerrors.AsKarşılaştırması - 8. Çoklu Hata Yönetimi:
errors.Join - 9. Panic ve Recover
- 10. Pratikte Hata Yönetimi Kalıpları
- 11. Loglama mı, Hata Döndürme mü?
- 12. Yaygın Anti-Pattern’ler
- 13. Hatalar İçin En İyi Uygulamalar Kontrol Listesi
- Bölüm III — Context ve Hataların Birleşimi
- Ek: Hızlı Referans Tabloları
Bölüm I — context Paketi
1. Context Neden Var?
Go 1.7’den önce (yani context paketi golang.org/x/net/context‘ten standart kütüphaneye taşınmadan önce), şu işlemleri yapmanın standart bir yolu yoktu:
- Uzun süren işlemleri iptal etmek (örneğin, istemci bağlantıyı kesmişse bir veritabanı sorgusunu terk etmek).
- Birden fazla fonksiyon çağrısı, goroutine ve hatta ağ sınırlarını (RPC) aşan bir çağrı zinciri boyunca deadline/timeout bilgisini yaymak.
- İstek kapsamlı (request-scoped) verileri (örneğin trace ID’leri) her fonksiyon imzasını değiştirmeden API sınırları boyunca taşımak.
Her ekip kendi geçici çözümünü icat ediyordu: bir done chan struct{}, bir time.Timer, özel Cancellable arayüzleri vb. Bu dağınıklık, kütüphanelerin birbiriyle uyumsuz olmasına yol açıyordu. context.Context, bunu tek bir arayüzde birleştirdi — bir Go programının her katmanı (HTTP handler’ları, gRPC servisleri, veritabanı sürücüleri, goroutine’ler) bu arayüzü anlayıp onunla işbirliği yapabiliyor.
Temel fikir: Context, önce veri taşımakla değil, önce iptal sinyali göndermekle, ikinci olarak da deadline yaymakla ilgilidir. Değer taşımak ise çok uzaktan üçüncü sıradadır ve genellikle fazla kullanılır (overuse edilir).
2. Context Arayüzü (Interface)
type Context interface {
// Deadline, bu context adına yapılan işin iptal edilmesi gereken zamanı döner.
// Eğer deadline belirlenmemişse ok==false döner.
Deadline() (deadline time.Time, ok bool)
// Done, bu context adına yapılan iş iptal edilmesi gerektiğinde kapanan
// bir kanal döner. Bu context asla iptal edilemiyorsa Done nil dönebilir.
Done() <-chan struct{}
// Err, Done'ın neden kapandığını açıklayan nil olmayan bir hata döner.
// Done henüz kapanmadıysa Err nil döner.
// Done kapandıysa Err, Canceled veya DeadlineExceeded döner.
Err() error
// Value, bu context için key'e karşılık gelen değeri döner,
// yoksa nil döner.
Value(key any) any
}
Dikkat edilmesi gereken noktalar:
Contextbir interface’tir, struct değildir. Farklı implementasyonları vardır (emptyCtx,cancelCtx,timerCtx,valueCtx).- Tüketici (consumer) açısından salt okunur (read-only) ve değiştirilemez (immutable) olacak şekilde tasarlanmıştır. Bir context’i asla mutasyona uğratmazsınız; her zaman bir üst (parent) context’ten yeni bir context türetirsiniz (derive).
Done()bir boolean değil, bir kanal döner; böylece doğrudan birselectifadesi içinde kullanılabilir — bu, idiomatik Go eşzamanlılığının (concurrency) bir parçasıdır.Err()size context’in neden iptal edildiğini söyler: yacontext.Canceled(açık/manuel iptal) ya dacontext.DeadlineExceeded(timeout/deadline aşımı).
Neden callback değil de kanal?
Go, callback kaydı yerine kanallar aracılığıyla kompozisyonu (composability) tercih eder. Bir <-chan struct{}, özel bir durum gerektirmeden diğer kanallarla (I/O, timer’lar, diğer done sinyalleri) birlikte select edilebilir. Bu, context‘i Go’nun diğer eşzamanlılık ilkellerine (primitive) dik (orthogonal) tutan bilinçli bir tasarım kararıdır.
3. Kök Context’ler
ctx1 := context.Background()
ctx2 := context.TODO()
context.Background(): Her context ağacının köküdür. Asla iptal edilmez, deadline’ı yoktur ve hiçbir değer taşımaz.main()içinde, testlerde ve bir isteği ya da uzun süreli bir süreci başlatırken en üst düzey context olarak kullanılır.context.TODO(): Çalışma zamanındaBackground()ile anlamsal olarak aynıdır, ancak bir niyeti işaret eder: “Bu fonksiyonun bir context alması gerektiğini biliyorum ama henüz bağlamadım, ya da hangi context’i kullanacağımdan emin değilim.” Refactoring için bir işaretleyicidir — grep ile bulunabilir ve geçici olması amaçlanmıştır.
Genel kural: Hangi context’i geçireceğinizden emin değilseniz ve gerçekten bir üst (parent) context yoksa TODO() kullanın. Bilinçli olarak en üst düzey bir context oluşturuyorsanız (örneğin main içinde), Background() kullanın.
4. İptal İşlemi: context.WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel, bir üst context’ten alt (child) bir context türetir. Yeni bir context ve bir cancel fonksiyonu döner. cancel() çağrıldığında:
- Döndürülen context’in
Done()kanalını kapatır. Err()‘ücontext.Canceledolarak ayarlar.- İptali, ondan türetilen tüm alt (descendant) context’lere yayar.
- Bellek sızıntılarını (memory leak) önlemek için alt context’i üst context’in takip yapılarından kaldırır.
Örnek
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d duruyor: %v\n", id, ctx.Err())
return
default:
// bir iş birimi yap
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(1 * time.Second)
cancel() // tüm worker'lara durma sinyali gönder
time.Sleep(500 * time.Millisecond)
}
Kritik kural: cancel()‘ı her zaman çağırın
Context “doğal olarak” bitse bile (örneğin işlem başarıyla tamamlansa bile), context ile ilişkili kaynakları serbest bırakmak için cancel()‘ı mutlaka çağırmalısınız — en önemlisi, Go çalışma zamanının iç durumu garbage-collect edebilmesi ve ilişkili timer’ı (WithTimeout/WithDeadline durumunda) durdurabilmesi için. İdiomatik kalıp şudur:
ctx, cancel := context.WithCancel(parent)
defer cancel()
Bunu unutmak, production Go servislerinde en yaygın goroutine ve bellek sızıntısı kaynaklarından biridir.
5. Timeout ve Deadline’lar
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithTimeout(parent, d), aslındaWithDeadline(parent, time.Now().Add(d))olarak implemente edilmiştir.WithDeadline, saat zamanıd‘ye ulaştığında ya dacancel()manuel olarak çağrıldığında ya da üst context iptal edildiğinde — hangisi önce gerçekleşirse — context’i otomatik olarak iptal eder.- Deadline’a ulaşıldığında,
ctx.Err()context.DeadlineExceededdöner.
Örnek: Timeout’lu HTTP çağrısı
func verileriGetir(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("istek oluşturulurken: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
// Timeout tetiklendiyse err, context.DeadlineExceeded'i sarmalar (wrap eder)
return nil, fmt.Errorf("istek gerçekleştirilirken: %w", err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Deadline yalnızca kısalır, asla uzamaz
Önemli, sıkça gözden kaçan bir kural: bir alt context’in etkin deadline’ı, üst context’inkinden asla daha geç olamaz. Üst context’in 5 saniyelik bir deadline’ı varsa ve WithTimeout(parent, 10*time.Second) çağırırsanız, ortaya çıkan context yine de üst context’in 5 saniyelik deadline’ına uyacaktır. Go, otomatik olarak ikisinden daha erken olanı alır.
parent, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
child, cancel2 := context.WithTimeout(parent, 10*time.Second)
defer cancel2()
// child yine de yaklaşık 5 saniye sonra iptal edilecek, 10 değil.
6. Değer Taşıma: context.WithValue
func WithValue(parent Context, key, val any) Context
Bu, istek kapsamlı verileri — bir istek ID’si, kimliği doğrulanmış bir kullanıcı veya bir trace span gibi — bir context’e ekleyerek, çağrı zincirinde daha aşağıda, her fonksiyon imzasını değiştirmeden okunabilmesini sağlar.
Key’leri doğru kullanma yolu
Anahtar (key) olarak asla düz string’ler veya built-in tipler kullanmayın — bunlar paketler arasında çakışabilir. Export edilmemiş özel bir tip tanımlayın:
package requestctx
type ctxKey int
const (
userIDKey ctxKey = iota
requestIDKey
)
func WithUserID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
func UserID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(userIDKey).(string)
return id, ok
}
Bu kalıp:
- Diğer paketlerde tanımlanan key’lerle çakışmayı önler (çünkü
ctxKeyexport edilmemiştir ve pakete özgüdür). - Çağıranları her yerde ham tip dönüşümü (type assertion) yapmaya zorlamak yerine, tip güvenli erişimci (accessor) fonksiyonlar sağlar.
- “Key namespace"ini sahibi olan pakete özel tutar.
WithValue Ne İçin Kullanılmalı — Ne İçin Kullanılmamalı
Uygun kullanımlar:
- İstek kapsamlı meta veriler: trace/span ID’leri, istek ID’leri, deadline ile ilgili veriler.
- Kontrol edemediğiniz katmanlar boyunca akması gereken kesişen (cross-cutting) endişeler (örneğin bir HTTP framework’ündeki middleware’in kimliği doğrulanmış bir kullanıcıyı eklemesi).
Uygunsuz kullanımlar (çok yaygın bir anti-pattern):
- Context üzerinden opsiyonel fonksiyon parametreleri geçirmek (“context’i bir God object gibi kullanmak”).
- Bir
*sql.DB, bir logger veya açık constructor/fonksiyon argümanı olması gereken diğer bağımlılıkları geçirmek. - Fonksiyonların gerçekten üzerinde işlem yaptığı iş mantığı verilerini geçirmek (bu veriler, okunabilirlik ve tip güvenliği için açık bir parametre olmalıdır).
Go ekibinden genel kural: “Context Value’ları yalnızca süreç ve API sınırlarını geçen istek kapsamlı veriler için kullanın, fonksiyonlara opsiyonel parametre geçirmek için değil.”
7. Context Yayılım (Propagation) Kuralları
İdiomatik kurallar (doğrudan context paketi dokümantasyonunda belirtilmiştir) şunlardır:
- Context, bir fonksiyonun ilk parametresi olmalıdır, geleneksel olarak
ctxadıyla:func BirŞeyYap(ctx context.Context, arg Arg) error - Bir Context’i asla bir struct içinde saklamayın. Bunun yerine, ihtiyaç duyan her metoda açıkça geçirin. (Nadir istisnalar vardır, örneğin generated code’da ya da struct’ın kendisinin tek bir isteğin yaşam döngüsünü temsil ettiği durumlarda — ama bu yine de istisnadır, kural değil.)
- Bir fonksiyon buna izin verse bile Context olarak asla
nilgeçirmeyin. Hangi context’i kullanacağınızdan emin değilsenizcontext.TODO()kullanın. - Karmaşık iptal mantığını yönettiğiniz durumlarda fonksiyona geçirilen context ilk kontrol edilen şey olmalıdır, ancak her basit fonksiyonun başında
ctx.Err()‘ü kontrol etmenize gerek yoktur — bu, esas olarak pahalı bir işe başlamadan önce önem taşır. - Bir
Context, birden fazla goroutine tarafından aynı anda eşzamanlı kullanım için güvenlidir. Aynı context’i birçok goroutine’e geçirebilirsiniz.
8. Context Ağaçları ve İptal Nasıl Yayılır
WithCancel, WithTimeout, WithDeadline veya WithValue‘ya yapılan her çağrı, üst context’ine bir referans tutan bir alt (child) context üretir. İç yapıda, iptal edilebilir context’ler (cancelCtx) kendilerini üst context’lerine kaydeder, böylece:
- Üst context iptal edilirse, tüm alt context’ler de iptal edilir (aşağı doğru zincirleme olarak).
- Bir alt context iptal edilirse, yalnızca o alt context ve onun altları iptal edilir — kardeşler (sibling) ve üst context etkilenmez.
Background()
└── WithCancel (A)
├── WithTimeout (B)
│ └── WithValue (C)
└── WithCancel (D)
A iptal edilirse → B, C ve D‘nin tamamı iptal edilir.
D iptal edilirse → yalnızca D iptal edilir; A, B, C etkilenmeden kalır.
Bu ağaç yapısı, context’i tüm bir alt sistem boyunca kapatmayı koordine etmek için bu kadar güçlü kılan şeydir: bir isteğin kökündeki tek bir context’i iptal edin, o isteğe bağlı her goroutine, her downstream RPC çağrısı ve her veritabanı sorgusu iptali gözlemleyip düzgünce sonlanabilir.
9. Context ile İlgili Yaygın Tuzaklar
Tuzak 1: cancel()‘ı çağırmayı unutmak
// KÖTÜ — iç timer ve context durumunu sızdırır
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
// İYİ
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Tuzak 2: Context’i bir struct alanında saklamak
// KÖTÜ
type Service struct {
ctx context.Context
}
// İYİ — ctx'i her çağrıda açıkça geçir
type Service struct{}
func (s *Service) IsYap(ctx context.Context) error { ... }
Tuzak 3: Temel parametreler için context value kullanmak
// KÖTÜ — önemli bir parametreyi gizler, derleyici güvenliği yok
ctx = context.WithValue(ctx, "kullaniciID", id)
func IsleV(ctx context.Context) { ... } // kullaniciID ctx içine gömülü
// İYİ
func IsleV(ctx context.Context, kullaniciID string) { ... }
Tuzak 4: Uzun döngülerde ctx.Done()‘ı kontrol etmemek
// KÖTÜ — iptali tamamen görmezden gelir
for i := 0; i < 1_000_000; i++ {
agirIs(i)
}
// İYİ
for i := 0; i < 1_000_000; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
agirIs(i)
}
Tuzak 5: Tipli, export edilmemiş key’ler yerine string key kullanmak
Aynı "userID" gibi string key’i kullanan ilgisiz paketler arasında istemsiz çakışmalara yol açar.
Tuzak 6: Gelen context’ten türetmek yerine yeni bir Background() oluşturmak
// KÖTÜ — tüm iptal zincirini kırar ve trace/değerleri düşürür
func Handler(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() // yanlış! r.Context() kullanılmalı
isYap(ctx)
}
// İYİ
func Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
isYap(ctx)
}
Tuzak 7: ctx.Done()‘ı hiç gözlemlemeyen goroutine’lerin sızdırılması
Bir goroutine bloklayan bir işlem yapıyorsa (örneğin hiç yazılmayacak bir kanaldan okuma) ve hiç ctx.Done() üzerinde select yapmıyorsa, işlem mantıksal olarak “terk edilmiş” olsa bile sonsuza kadar sızabilir.
10. HTTP Sunucu ve İstemcilerde Context
Sunucu tarafı
Gelen her *http.Request, r.Context() üzerinden erişilebilen bir context otomatik olarak taşır. Bu context, net/http sunucusu tarafından şu durumlarda otomatik olarak iptal edilir:
- İstemci bağlantıyı keserse.
ResponseWriter‘ın alt bağlantısı kapanırsa.- (HTTP/2’de) stream iptal edilirse.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
sonuc, err := pahaliSorgu(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// istemci gitti; genellikle geri yazacak bir şey yok
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(sonuc)
}
Middleware kullanarak isteğin context’inden daha katı bir timeout da türetebilirsiniz:
func withTimeout(next http.Handler, d time.Duration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
İstemci tarafı
http.NewRequestWithContext, giden bir isteğe bir context ekler. Context iptal edilirse ya da deadline’ı geçerse, devam eden HTTP isteği iptal edilir ve http.Client.Do, context’in hatasını sarmalayan bir hata döner.
11. Goroutine’ler ve errgroup ile Context
Çok yaygın bir kalıp: birden çok goroutine’i paralel başlatmak ve herhangi biri başarısız olursa geri kalanları iptal etmek. golang.org/x/sync/errgroup paketi bunu context üzerine inşa eder:
import "golang.org/x/sync/errgroup"
func tumunuGetir(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
sonuclar := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // döngü değişkenlerini yakala (Go 1.22 öncesi idiom)
g.Go(func() error {
veri, err := verileriGetir(ctx, url)
if err != nil {
return err // gruptaki diğer tüm goroutine'ler için ctx'i iptal eder
}
sonuclar[i] = veri
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return sonuclar, nil
}
Burada errgroup.WithContext, herhangi bir goroutine nil olmayan bir hata döndürdüğü anda otomatik olarak iptal edilen türetilmiş bir context döner — devam eden diğer her goroutine bunu ctx.Done() üzerinden gözlemleyebilir ve işini erkenden terk ederek kaynak tasarrufu sağlayabilir.
12. Context İçin En İyi Uygulamalar Kontrol Listesi
- ✅ Her zaman
context.Context‘i ilk parametre olarak,ctxadıyla kabul edin. - ✅
WithCancel/WithTimeout/WithDeadline‘ın döndürdüğücancelfonksiyonunu her zaman çağırın, genellikledeferile. - ✅ Context’leri gelen context’ten türetin (
r.Context(), üst ctx) — zincirin ortasında asla yeni birBackground()üretmeyin. - ✅
WithValueiçin export edilmemiş özel key tipleri kullanın. - ✅
WithValue‘yu yalnızca kesişen, istek kapsamlı meta veriler için ayırın. - ✅ Uzun döngülerde veya pahalı işlemlerden önce/sonra
ctx.Done()‘ı kontrol edin. - ✅ Bir
Context‘i asla bir struct alanında saklamayın (açıkça geçirin). - ✅ Asla
nilcontext geçirmeyin —context.TODO()kullanın. - ✅ Unutmayın: bir context değiştirilemezdir (immutable) — yeni bir context türetmek üst context’i değiştirmez.
- ✅
context.Canceledvecontext.DeadlineExceeded‘i beklenen, “normal” sonuçlar olarak ele alın; her zaman hata olarak loglanması gerekmez.
Bölüm II — Go’da Hata Yönetimi
1. Felsefe: Hatalar Birer Değerdir
Go, olağan hata yönetimi için bilinçli olarak exception (istisna) kullanmaz (panic/recover‘ı vardır, ama bu gerçekten istisnai, kurtarılamaz durumlar için ayrılmıştır — aşağıya bakınız). Bunun yerine, Go hataları fonksiyonlardan döndürülen sıradan değerler olarak ele alır, en idiomatik biçimde son dönüş değeri olarak:
func Bol(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("sıfıra bölme")
}
return a / b, nil
}
Go’nun kurucularından Rob Pike’ın ünlü sözü: “Hatalar birer değerdir ve programlanabilirler.” Bu şu anlama gelir:
- Hataları diğer değerler gibi sarmalayabilir, inceleyebilir, karşılaştırabilir ve dönüştürebilirsiniz.
- Etraflarında sıradan Go kontrol akışı kullanarak özel mantık (retry politikaları, hata sınıflandırması vb.) kurabilirsiniz — özel exception-handling sözdizimine gerek yoktur.
Bu, çağıranı her olası hata noktasını açıkça kabul etmeye ve ele almaya zorlar:
sonuc, err := Bol(10, 0)
if err != nil {
// hatayı ele al — derleyici bunu zorlamaz, ama idiom zorlar
log.Fatal(err)
}
fmt.Println(sonuc)
Bu, daha ayrıntılı (verbose) koda yol açsa da (sıkça tekrarlanan ünlü if err != nil kalıbı), kontrol akışını açık ve izlenebilir kılar — birkaç stack frame ötede yakalanabilecek “görünmez” bir atlama (fırlatılan bir exception gibi) yoktur.
2. error Arayüzü
type error interface {
Error() string
}
Hepsi bu kadar — dikkat çekici derecede küçük. Error() string metodunu implemente eden herhangi bir tip, error arayüzünü tatmin eder. Bu basitlik kasıtlıdır: kendisini bir string olarak tanımlayabildiği sürece herhangi bir tip — bir struct, bir string sarmalayıcı, bir tamsayı kodu, hatta bir fonksiyon tipi — bir hata olabilir.
type BenimHatam struct {
Kod int
Msj string
}
func (e *BenimHatam) Error() string {
return fmt.Sprintf("hata %d: %s", e.Kod, e.Msj)
}
var err error = &BenimHatam{Kod: 404, Msj: "bulunamadı"}
fmt.Println(err) // hata 404: bulunamadı
Nil interface tuzağı
Çok yaygın bir hata: Tipli bir nil pointer’ı bir error arayüz değeri olarak döndürmek, nil olmayan bir arayüz ile sonuçlanır; çünkü arayüz hem bir tip hem bir değer taşır ve yalnızca değer nil’dir.
func basarisizOlabilir() *BenimHatam {
return nil // hata oluşmadı
}
func birSeyYap() error {
var err *BenimHatam = basarisizOlabilir()
return err // TEHLİKE: bu, nil olmayan bir error arayüzü döndürür!
}
func main() {
if err := birSeyYap(); err != nil {
fmt.Println("Bir hata alındı:", err) // gerçek bir hata olmasa bile bu satır çalışır!
}
}
Neden: birSeyYap()‘in dönüşündeki err‘ün dinamik tipi *BenimHatam, dinamik değeri ise nil‘dir. Literal nil arayüzüyle (tip=nil, değer=nil) karşılaştırıldığında, EŞİT DEĞİLDİR; çünkü tip kısmı farklıdır.
Çözüm: error arayüzünü doğrudan döndürün ve hata yoksa açıkça nil döndürün:
func birSeyYap() error {
if err := basarisizOlabilir(); err != nil {
return err
}
return nil
}
3. Hata Oluşturma
errors.New
err := errors.New("bir şeyler ters gitti")
Mesajı asla değişmeyen, basit, statik bir hata oluşturur.
fmt.Errorf
err := fmt.Errorf("kullanıcı %d işlenirken hata: %v", kullaniciID, sebep)
Biçimlendirilmiş bir hata mesajı oluşturmanıza olanak tanır — ama en önemlisi, fmt.Errorf aynı zamanda %w fiilini (verb) de destekler (bölüm 6’da açıklanmıştır); bu sayede alttaki hataya programatik erişimi koruyarak onu sarmalayabilirsiniz (wrap).
4. Sentinel (Nöbetçi) Hatalar
Bir sentinel hata, çağıranların doğrudan karşılaştırabileceği, önceden tanımlanmış belirli bir hata değeridir:
package sql
var ErrNoRows = errors.New("sql: sonuç kümesinde satır yok")
Kullanım:
row := db.QueryRow("SELECT ...")
var isim string
err := row.Scan(&isim)
if errors.Is(err, sql.ErrNoRows) {
// özellikle "bulunamadı" durumunu ele al
}
Sentinel hatalar için kurallar:
- İsimlerini
Errön ekiyle verin:ErrNotFound,ErrPermission,io.EOF(tarihsel nedenlerle isimlendirmede bir istisna). - Bunları
constdeğil, paket düzeyindevarolarak tanımlayın (çünküerrorbir arayüzdür ve arayüzler sabit (constant) olamaz). - Az kullanın — sentinel hatalar paketler arasında sıkı bağımlılık (tight coupling) yaratır (çağıranın, sizin belirli hata değerinizle karşılaştırma yapmak için paketinizi import etmesi gerekir). Bunları esas olarak gerçekten evrensel, iyi bilinen durumlar için tercih edin (örneğin
io.EOF,sql.ErrNoRows,context.Canceled).
5. Özel Hata Tipleri
Hatayla birlikte yapılandırılmış veri (sadece bir string değil) taşımanız gerektiğinde, özel bir tip tanımlayın:
type DogrulamaHatasi struct {
Alan string
Msj string
}
func (e *DogrulamaHatasi) Error() string {
return fmt.Sprintf("%q alanında doğrulama başarısız: %s", e.Alan, e.Msj)
}
Çağıranlar, yapılandırılmış veriyi errors.As kullanarak çıkarabilir (bkz. bölüm 7):
var dhErr *DogrulamaHatasi
if errors.As(err, &dhErr) {
fmt.Println("Sorunlu alan:", dhErr.Alan)
}
Receiver pointer mı, değer mi olmalı?
Kural: hata tipi mutasyona uğrayabilen veya büyük bir iç duruma sahipse, ya da iki hata arasındaki kimlik karşılaştırmasının (==) “aynı olay” anlamına gelmesini istiyorsanız, Error()‘ı pointer receiver (*DogrulamaHatasi) üzerinde implemente edin. Küçük, tamamen değiştirilemez (immutable) hata tipleri için değer receiver kullanın. Gerçek dünyadaki kodların çoğu, özel hata struct’ları için pointer receiver kullanır.
6. Hata Sarmalama (%w) ve Açma (Unwrap)
Go 1.13’te tanıtılan hata sarmalama (error wrapping), bir hataya bağlam ekleyebilmenizi sağlarken, orijinal alttaki hatayı programatik olarak inceleme yeteneğini de korur.
func konfigYukle(yol string) error {
veri, err := os.ReadFile(yol)
if err != nil {
return fmt.Errorf("%s konumundan konfig yüklenirken: %w", yol, err)
}
_ = veri
return nil
}
Burada %w, err‘ü (örneğin bir *fs.PathError) yeni bir hatanın içine sarmalar, ancak yine de çağıranın onu “unwrap” etmesine izin verir. Bunu, sadece mesajı koruyacak ve orijinal hatanın kimliğini ile tipini kaybedecek olan %v ile karşılaştırın.
Unwrap() metodu
Sarmalama çalışır çünkü fmt.Errorf("...: %w", err) şunu implemente eden bir değer döner:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string { return e.msg }
func (e *wrapError) Unwrap() error { return e.err }
Herhangi bir özel hata tipi, yalnızca Unwrap() error‘ı implemente ederek sarmalamayı destekleyebilir:
type SorguHatasi struct {
Sorgu string
Err error
}
func (e *SorguHatasi) Error() string { return fmt.Sprintf("sorgu %q başarısız: %v", e.Sorgu, e.Err) }
func (e *SorguHatasi) Unwrap() error { return e.Err }
Birden fazla hatayı sarmalamak (%w ile birden fazla, Go 1.20+)
Go 1.20’den itibaren fmt.Errorf, birden fazla %w fiilini destekler:
err := fmt.Errorf("birden fazla hata: %w ve %w", err1, err2)
Bu, Unwrap()‘i []error{err1, err2} döndüren bir hata oluşturur (errors.Join ile birlikte tanıtılan interface{ Unwrap() []error } şeklini implemente eder).
7. errors.Is ve errors.As Karşılaştırması
Her ikisi de Go 1.13’te tanıtılan bu iki fonksiyon, sarmalanmış hata zincirlerini incelemek için birincil araçlardır.
errors.Is — “bu hata bu belirli değer mi (veya onu sarmalıyor mu)?”
func Is(err, target error) bool
Sarmalanmış hataların zincirini (tekrarlanan Unwrap() çağrıları ile) dolaşır ve herhangi birinin target‘a == olup olmadığını, YA DA Is(error) bool‘u implemente edip bir eşleşme bildirip bildirmediğini kontrol eder.
if errors.Is(err, sql.ErrNoRows) {
// err = fmt.Errorf("sorgu başarısız: %w", sql.ErrNoRows) olsa bile eşleşir
}
if errors.Is(err, context.DeadlineExceeded) {
// timeout'u özellikle ele al
}
Sentinel hata karşılaştırmaları için errors.Is kullanın.
errors.As — “bu hata zincirinde bu tipte bir hata var mı, varsa bana ver?”
func As(err error, target any) bool
Zinciri dolaşır ve somut tipi target (istenen tipe bir pointer) ile eşleşen ilk hatayı bulur, bulursa atama yapar.
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("başarısız yol:", pathErr.Path)
}
Belirli bir özel hata tipinden, ne kadar derinlemesine sarmalanmış olursa olsun yapılandırılmış alanları çıkarmanız gerektiğinde errors.As kullanın.
Genel kural
| Durum | Kullanın |
|---|---|
| Bilinen, belirli bir hata değeriyle (sentinel) karşılaştırma | errors.Is |
| Belirli bir hata tipinin alanlarını çıkarma | errors.As |
| Sarmalanmamış hatalarda basit eşitlik kontrolü (nadir, önerilmez) | == (yalnızca sarmalama olmadığından eminseniz) |
Özel Is/As davranışı implemente etmek
Şunu implemente ederek eşleştirme mantığını özelleştirebilirsiniz:
func (e *BenimHatam) Is(target error) bool {
t, ok := target.(*BenimHatam)
if !ok {
return false
}
return e.Kod == t.Kod // koda göre eşleştir, mesajı yok say
}
8. Çoklu Hata Yönetimi: errors.Join
Go 1.20‘de tanıtılan errors.Join, birden fazla hatayı tek bir hata değerinde birleştirir:
func Join(errs ...error) error
err1 := errors.New("disk dolu")
err2 := errors.New("ağa ulaşılamıyor")
birlesik := errors.Join(err1, err2)
fmt.Println(birlesik)
// disk dolu
// ağa ulaşılamıyor
fmt.Println(errors.Is(birlesik, err1)) // true
fmt.Println(errors.Is(birlesik, err2)) // true
Bu, ilk hatada durmak yerine tüm hataları bir kerede bildirmek istediğiniz, birden fazla alanı doğrulama gibi senaryolarda son derece kullanışlıdır:
func dogrula(u Kullanici) error {
var hatalar []error
if u.Isim == "" {
hatalar = append(hatalar, errors.New("isim gerekli"))
}
if u.Yas < 0 {
hatalar = append(hatalar, errors.New("yaş negatif olamaz"))
}
return errors.Join(hatalar...) // hatalar boşsa nil döner
}
Not: errors.Join(nil, nil) nil döner. errors.Join, nil girdileri otomatik olarak atlar ve tüm girdiler nil ise, birleştirilen sonucun kendisi de nil olur.
9. Panic ve Recover
panic ve recover, normal hata yönetiminin yerine geçmez. Bunlar şunlar için ayrılmıştır:
- Gerçekten kurtarılamaz programlama hataları (örneğin, index sınırın dışında, nil pointer dereference, başarısız bir invariant/assertion).
- Yürütmeye devam etmenin güvensiz veya anlamsız olacağı durumlar.
- Framework düzeyinde “başlatma sırasında hızlı başarısız ol” senaryoları.
Temel mekanik
func guvenliBol(a, b int) (sonuc int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic'ten kurtarıldı: %v", r)
}
}()
sonuc = a / b // b == 0 ise panic atar (tamsayı sıfıra bölme)
return
}
panic(v any), mevcut fonksiyonun normal yürütülmesini hemen durdurur, mevcut goroutine’deki tüm deferred fonksiyonları (LIFO sırasında) çalıştırır ve ya bir deferred fonksiyon içinderecover()çağrılana kadar ya da program bir stack trace ile çökene kadar çağrı yığınında (call stack) yukarı doğru yayılır.recover()yalnızca doğrudan bir deferred fonksiyon içinde çağrıldığında etkilidir. Başka bir yerde çağrılırsanildöner ve hiçbir şey yapmaz.
Panic ne zaman kullanılmalı (ve kullanılmamalı)
Uygun:
func mustCompile(desen string) *regexp.Regexp {
re, err := regexp.Compile(desen)
if err != nil {
panic(err) // programcı hatası: desen sabit kodlanmış ve bozuk olduğu bilinen
}
return re
}
Bu, regexp.MustCompile, template.Must gibi kalıpları yansıtır — başarısızlığın bir hata (bug) anlamına geldiği, kurtarılması gereken bir çalışma zamanı koşulu olmadığı paket düzeyinde başlatma (initialization) için kullanılır.
Uygun değil:
// KÖTÜ — olağan, beklenen başarısızlık durumları için panic kullanmak
func KullaniciGetir(id string) *Kullanici {
kullanici, ok := db[id]
if !ok {
panic("kullanıcı bulunamadı") // bunun yerine bir hata döndürmeli!
}
return kullanici
}
Goroutine’lerde kurtarma (recover)
Bir goroutine’deki recover(), başka bir goroutine’deki panic’i yakalayamaz. Panic atabilecek her goroutine’in kendi deferred recover’ına ihtiyacı vardır:
func worker(id int) {
defer func() {
if r := recover(); r != nil {
log.Printf("worker %d kurtarıldı: %v", id, r)
}
}()
riskliIsYap()
}
go worker(1) // eğer bu, kendi defer/recover'ı olmadan panic atarsa, tüm programı çökertir
Önemli: Herhangi bir goroutine’de kurtarılmamış bir panic, yalnızca o goroutine’i değil, tüm programı çökertir. Bu, bilinçli bir Go tasarım kararıdır — bir panic genellikle bozuk bir invariant’ı gösterir ve Go, sessiz bozulma yerine gürültülü bir çökmeyi tercih eder.
10. Pratikte Hata Yönetimi Kalıpları
Kalıp: Yığın boyunca yayılırken bağlam eklemek
func KonfigOku(yol string) (*Konfig, error) {
veri, err := os.ReadFile(yol)
if err != nil {
return nil, fmt.Errorf("konfig dosyası okunurken %q: %w", yol, err)
}
var cfg Konfig
if err := json.Unmarshal(veri, &cfg); err != nil {
return nil, fmt.Errorf("konfig dosyası ayrıştırılırken %q: %w", yol, err)
}
return &cfg, nil
}
Her katman, %w sayesinde orijinal hatanın kimliğini atmadan, biraz bağlam ekler (“bu başarısız olduğunda ne yapmaya çalışıyordum”).
Kalıp: Hataları kontrol akışı için sınıflandırmak (örneğin HTTP durum kodu eşleme)
var (
ErrBulunamadi = errors.New("kaynak bulunamadı")
ErrYetkisiz = errors.New("yetkisiz")
)
func hataYonet(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrBulunamadi):
http.Error(w, err.Error(), http.StatusNotFound)
case errors.Is(err, ErrYetkisiz):
http.Error(w, err.Error(), http.StatusUnauthorized)
default:
http.Error(w, "sunucu hatası", http.StatusInternalServerError)
}
}
Kalıp: Geçici hatalarda yeniden deneme (retry)
type GeciciHata struct{ Err error }
func (e *GeciciHata) Error() string { return e.Err.Error() }
func (e *GeciciHata) Unwrap() error { return e.Err }
func geciciMi(err error) bool {
var t *GeciciHata
return errors.As(err, &t)
}
func yenidenDenemeliCalistir(ctx context.Context, fn func() error) error {
var err error
for deneme := 0; deneme < 3; deneme++ {
if err = fn(); err == nil {
return nil
}
if !geciciMi(err) {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(deneme+1) * 200 * time.Millisecond):
}
}
return fmt.Errorf("tüm yeniden denemeler başarısız oldu: %w", err)
}
Kalıp: Defer tabanlı temizlik ile hata yönetimi
func dosyaIsle(yol string) (err error) {
f, err := os.Open(yol)
if err != nil {
return fmt.Errorf("açılamadı %q: %w", yol, err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("kapatılamadı %q: %w", yol, cerr)
}
}()
// ... f'yi işle ...
return nil
}
Bu kalıp, dosyayı kapatmanın başarısız olmasının sessizce yok sayılmamasını sağlar, ancak daha önemli bir önceki hatayı da ezmez.
11. Loglama mı, Hata Döndürme mü?
Go kod tabanlarında önemli bir disiplin: her katmanda aynı hatayı hem loglamayın HEM de döndürmeyin. Bu, tekrarlayan, gürültülü loglara neden olur (aynı kök neden, 5 katmandan geçerken 5 kez loglanır).
Rehber:
- Düşük seviye/kütüphane kodu: hataları döndürün (muhtemelen
%wile sarmalayarak), loglamayın. - Yüksek seviye/sınır kodu (örneğin bir HTTP handler’ının en üstü, bir
main()fonksiyonu, bir mesaj kuyruğu tüketicisinin giriş noktası): nihai hatayı tam bağlamla bir kez loglayın ve ne yapılacağına karar verin (yeniden deneme, bir HTTP durumuyla yanıt verme, uyarı gönderme vb.).
// kütüphane katmanı — sadece döndür
func kullaniciGetir(ctx context.Context, id string) (*Kullanici, error) {
u, err := db.Query(ctx, id)
if err != nil {
return nil, fmt.Errorf("kullaniciGetir(%s): %w", id, err)
}
return u, nil
}
// sınır katmanı — burada, bir kez logla
func handleKullaniciGetir(w http.ResponseWriter, r *http.Request) {
u, err := kullaniciGetir(r.Context(), r.PathValue("id"))
if err != nil {
log.Printf("handleKullaniciGetir başarısız: %v", err)
http.Error(w, "sunucu hatası", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(u)
}
12. Yaygın Anti-Pattern’ler
Anti-pattern: Hataları yutmak (swallow etmek)
// KÖTÜ
veri, _ := os.ReadFile("config.json")
Bir hatayı sessizce göz ardı etmek, gerçek başarısızlıkları gizler ve kafa karıştırıcı downstream hatalara yol açar (örneğin veri‘nin nil olması).
Anti-pattern: Sarmalanmış hataları == ile karşılaştırmak
// KÖTÜ — hata fmt.Errorf("...: %w", err) ile sarmalandığı anda bozulur
if err == sql.ErrNoRows { ... }
// İYİ
if errors.Is(err, sql.ErrNoRows) { ... }
Anti-pattern: Aşırı sarmalama / gereksiz bağlam
// KÖTÜ — her katmanda tekrarlayan, faydasız bağlam
return fmt.Errorf("hata: %w", fmt.Errorf("hata: %w", fmt.Errorf("başarısız: %w", err)))
Her sarmalama, sadece “hata oluştu” gibi genel kelimeleri tekrarlamak yerine yeni, faydalı bilgi eklemelidir (ne yapılmaya çalışılıyordu).
Anti-pattern: Beklenen, kurtarılabilir durumlar için panic kullanmak
Bölüm 9’da zaten ele alındı — panic’i programcı hataları ve kurtarılamaz durumlar için ayırın, “kullanıcı girdisi geçersiz” veya “kayıt bulunamadı” gibi şeyler için değil.
Anti-pattern: Genel (public) API’lerden kararlı sentinel değerleri olmadan çıplak/gruplanmamış hatalar döndürmek
Paketinizi çağıranların hata durumlarını programatik olarak ayırt etmesi gerekiyorsa, sentinel hatalar veya tipli hatalar açığa çıkarın — onları err.Error() üzerinde string eşleştirme yapmaya zorlamayın.
Anti-pattern: “Nil pointer taşıyan nil olmayan interface” tuzağını göz ardı etmek
Bölüm 2’de ele alındı — somut bir pointer tipini error tipli bir dönüş üzerinden döndürürken her zaman dikkatli olun.
13. Hatalar İçin En İyi Uygulamalar Kontrol Listesi
- ✅ Hataları son dönüş değeri olarak döndürün; hemen
if err != nilile kontrol edin. - ✅ Hata zincirini korurken sarmalamak için
fmt.Errorf("...: %w", err)kullanın. - ✅ Sentinel karşılaştırmaları için
errors.Is, tipli hataları çıkarmak içinerrors.Askullanın. - ✅ Yalnızca iyi bilinen, evrensel durumlar için sentinel hatalar (
ErrXxx) tanımlayın. - ✅ Mesajla birlikte yapılandırılmış veriye ihtiyacınız olduğunda özel hata struct’ları tanımlayın.
- ✅ Hataları sınır katmanında bir kez loglayın — geçtikleri her katmanda değil.
- ✅
panic/recover‘ı programcı hataları ve kurtarılamaz durumlar için ayırın, rutin başarısızlıklar için değil. - ✅ Kasıtlı, belgelenmiş bir gerekçe olmadan bir hatayı asla sessizce atmayın (
_ = err). - ✅ Sarmalarken anlamlı bağlam ekleyin — sadece “hata” değil, ne yapıyordunuz onu belirtin.
- ✅ Somut hata tiplerini döndürürken arayüzdeki tipli-nil tuzağına dikkat edin.
- ✅ Birden fazla bağımsız hata için, yalnızca ilkini bildirmek yerine
errors.Joinkullanın.
Bölüm III — Context ve Hataların Birleşimi
Context ve hata yönetimi, gerçek Go sistemlerinde sürekli kesişir. Birkaç temel entegrasyon noktası:
İptali deadline’dan ayırt etmek
sonuc, err := isYap(ctx)
if err != nil {
switch {
case errors.Is(err, context.Canceled):
// çağıran vazgeçti / istemci bağlantıyı kesti — genellikle uyarı verilecek "gerçek" bir hata değil
case errors.Is(err, context.DeadlineExceeded):
// işlem çok uzun sürdü — uyarı vermek, yeniden denemek veya HTTP 504 döndürmek isteyebilirsiniz
default:
// başka bir başarısızlık
}
}
Context iptalini kendi hata zincirinizin bir parçası olarak yaymak
Bir fonksiyon ctx‘e saygı gösteren bir iş yapıyorsa ve iptal nedeniyle erken dönüyorsa, basitçe ctx.Err()‘ü döndürmek (veya sarmalamak) idiomatiktir:
func uzunIslem(ctx context.Context) error {
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
return fmt.Errorf("uzunIslem kesintiye uğradı: %w", ctx.Err())
default:
}
// bir parça iş yap
}
return nil
}
Bu, çağıranın, iptalin sarmalanmış çağrılar zincirinin ne kadar derininde gerçekleştiğine bakılmaksızın errors.Is(err, context.Canceled) kullanmasına olanak tanır.
“Beklenen” bir hata biçimi olarak timeout’lar
Tıpkı context.Canceled‘in genellikle çökmeye değer bir hata olarak ele alınmaması gerektiği gibi, bir context.DeadlineExceeded da yük altında sıkça normal, beklenen bir sonuçtur — bunu mutlaka bir stack trace gerektiren bir şey olarak değil, üzerine dallanma (branch) yapılacak bir değer olarak ele alın.
Ek: Hızlı Referans Tabloları
Context fonksiyonları
| Fonksiyon | Amaç |
|---|---|
context.Background() | Kök context, asla iptal edilmez, değer taşımaz |
context.TODO() | Yer tutucu kök context, “henüz karar verilmedi” işaretini verir |
context.WithCancel(parent) | Manuel iptal |
context.WithTimeout(parent, d) | d süresi sonunda iptal eder |
context.WithDeadline(parent, t) | t saat zamanında iptal eder |
context.WithValue(parent, key, val) | İstek kapsamlı veri ekler |
context.Cause(ctx) (Go 1.21+) | WithCancelCause üzerinden bile alttaki nedeni döner |
context.WithCancelCause(parent) (Go 1.21+) | Özel bir hata nedeniyle iptal |
Hata fonksiyonları
| Fonksiyon | Amaç |
|---|---|
errors.New(msg) | Basit, statik bir hata oluştur |
%w ile fmt.Errorf(format, ..., err) | Bir mesajı biçimlendirirken hatayı sarmala |
errors.Is(err, target) | err‘ün zincirin herhangi bir yerinde bir sentinel ile eşleşip eşleşmediğini kontrol et |
errors.As(err, &target) | Zincirden belirli bir hata tipini çıkar |
errors.Unwrap(err) | Manuel olarak bir seviye aç (unwrap) |
errors.Join(errs...) (Go 1.20+) | Birden fazla hatayı tek bir hatada birleştir |
Panic/recover hızlı referansı
| Kavram | Davranış |
|---|---|
panic(v) | Normal akışı durdurur, deferred fonksiyonları çalıştırır, yukarı doğru yayılır |
recover() | Yalnızca bir deferred fonksiyon içinde etkilidir; panic’i durdurur |
| Kurtarılmamış panic | Tüm programı çökertir (tüm goroutine’ler) |
| Goroutine’ler arası recover | Mümkün değildir; her goroutine’in kendi recover’ı gerekir |
Dokümanın sonu.