Go (Golang) — Context ve Hata Yönetimi: Çok Detaylı Bir Rehber

19 Ağustos 2026 · netologist · 25 dakika, 5178 kelime ·

Bu doküman, idiomatik Go programlamanın en önemli — ve en çok yanlış anlaşılan — iki konusunu ele alıyor: context paketi 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

  1. Bölüm I — context Paketi
  2. Bölüm II — Go’da Hata Yönetimi
  3. Bölüm III — Context ve Hataların Birleşimi
  4. 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:

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:

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()

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:

  1. Döndürülen context’in Done() kanalını kapatır.
  2. Err()‘ü context.Canceled olarak ayarlar.
  3. İptali, ondan türetilen tüm alt (descendant) context’lere yayar.
  4. 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)

Ö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:

WithValue Ne İçin Kullanılmalı — Ne İçin Kullanılmamalı

Uygun kullanımlar:

Uygunsuz kullanımlar (çok yaygın bir anti-pattern):

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:

  1. Context, bir fonksiyonun ilk parametresi olmalıdır, geleneksel olarak ctx adıyla:
    func BirŞeyYap(ctx context.Context, arg Arg) error
    
  2. 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.)
  3. Bir fonksiyon buna izin verse bile Context olarak asla nil geçirmeyin. Hangi context’i kullanacağınızdan emin değilseniz context.TODO() kullanın.
  4. 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.
  5. 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:

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:

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


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:

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:

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

DurumKullanın
Bilinen, belirli bir hata değeriyle (sentinel) karşılaştırmaerrors.Is
Belirli bir hata tipinin alanlarını çıkarmaerrors.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:

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 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:

// 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


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ı

FonksiyonAmaç
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ı

FonksiyonAmaç
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ı

KavramDavranış
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ış panicTüm programı çökertir (tüm goroutine’ler)
Goroutine’ler arası recoverMümkün değildir; her goroutine’in kendi recover’ı gerekir

Dokümanın sonu.