İçeriğe geç
Muhammet Şafak
Diller 4 dk okuma

Go'da hata yönetimi: error değeriyle yaşamak

İstisna (exception) mekanizması olmayan Go'da hata yönetimini dil deyimi olarak benimsemenin pratik yolu.


Go’ya PHP’den geçtiğimde en çok kafayı yorduğum şey hata yönetimi oldu. Exception tabanlı bir dilden gelince Go’nun yaklaşımı ilk başta garip hissettiriyor — hatta kasıtlı olarak kısıtlayıcı. Zamanla bu yaklaşımın neden var olduğunu ve nasıl doğal hale geldiğini anlamak, dilin diğer parçalarını da daha iyi kavramama yardımcı oldu.

Go’da hata nasıl çalışır

Go’da error bir interface’tir. Tek bir metodundan oluşur:

type error interface {
    Error() string
}

Hata üretebilecek her fonksiyon, son dönüş değeri olarak error döner. Çağıran kod bu değeri kontrol etmekten sorumludur:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("sıfıra bölme hatası")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Hata:", err)
        return
    }
    fmt.Println("Sonuç:", result)
}

Exception mekanizması yok. Hata, sıradan bir değer. Fonksiyon çağrısından çıkan değerlerin bir parçası.

if err != nil neden bu kadar tekrar ediyor

Go kodu okurken if err != nil bloklarının her yerde tekrar ettiğini görürsünüz. Bu şikâyet sık duyuluyor. Ama bunu bir eksiklikten ziyade kasıtlı bir karar olarak okumak gerekiyor.

PHP veya Java’da bir istisna fırlatıldığında, programın hangi noktada durduğu ve hangi catch bloğunun devreye girdiği her zaman net değildir. Exception, call stack’i atlayarak yukarıya tırmanır. Gözden kaçması kolaydır.

Go’da hata, tam olarak üretildiği noktada elimde. Onu görmezden gelmek aktif bir tercih — ve derleyici (compiler) bunu zorlamaz ama linter uyarır. Hatayı ele almak bir seçim değil, akışın bir parçası.

Bunu PHP’de yaşadığım somut bir durumla karşılaştırayım: bir dış API çağrısı sessizce başarısız oldu, istisna bir üst katmanda yakalandı ama loglama eksikti. Sorunu ancak kullanıcı şikâyeti gelince fark ettim. Go’da bu çağrı şöyle yazılırdı ve hata yayılacak yer açıkça görünürdü:

resp, err := apiClient.Call(payload)
if err != nil {
    return fmt.Errorf("dış API çağrısı başarısız: %w", err)
}

Hata sarmalamak (wrapping)

Go 1.13 ile gelen fmt.Errorf ve %w verb, hata bağlamını zenginleştirmenin standart yolu haline geldi:

func loadUserConfig(userID int) (Config, error) {
    data, err := readFile(fmt.Sprintf("config/%d.json", userID))
    if err != nil {
        return Config{}, fmt.Errorf("loadUserConfig: kullanıcı %d için yapılandırma okunamadı: %w", userID, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("loadUserConfig: JSON ayrıştırma hatası: %w", err)
    }

    return cfg, nil
}

%w sayesinde orijinal hata sarmalanır ama errors.Is ve errors.As ile hâlâ sorgulanabilir:

cfg, err := loadUserConfig(42)
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        // Dosya yok durumunu özel olarak ele al
    }
    log.Printf("Yapılandırma yüklenemedi: %v", err)
    return
}

Sarmalama ile doğrudan döndürme arasındaki seçim önemli. return err yaptığınızda orijinal hata bilgisi korunuyor ama bağlam yok. fmt.Errorf ile sarmak, hata mesajına “kim, ne, nerede” bilgisi eklemenizi sağlıyor. Uzun bir çağrı zincirinizde hata mesajının neyin nerede başarısız olduğunu söylemesi, log içinde kayıp bir satır aramaktan çok daha hızlı.

Özel hata tipleri

Yalnızca mesaj taşımak yetmediğinde kendi error tipinizi tanımlayabilirsiniz:

type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s bulunamadı (id: %d)", e.Resource, e.ID)
}

func findUser(id int) (*User, error) {
    user := db.Find(id)
    if user == nil {
        return nil, &NotFoundError{Resource: "Kullanıcı", ID: id}
    }
    return user, nil
}

Çağıran taraf errors.As ile bu tip üzerinden dal açabilir:

u, err := findUser(99)
if err != nil {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        // 404 yanıt ver
    }
    // Diğer hatalar
}

PHP geliştiricisi olarak uyum süreci

Exception tabanlı bir dilden gelince en büyük alışkanlık değişimi şu: hataları görmezden gelmek artık pasif değil, aktif bir tercih gerektiriyor. _ ile hata dönüşünü ıskalamak Go topluluğunda kötü pratik kabul ediliyor — ve haklı olarak.

İkinci büyük fark: her hata ayrı ayrı ele alınıyor. PHP’de geniş bir try-catch bloğuyla on farklı hatayı tek noktada yakalıyorsunuz. Go’da her fonksiyon çağrısının yanında bir kontrol var. Başta fazla görünüyor; zamanla hatanın tam olarak nerede ve ne zaman oluştuğunu görmenin değerini anlıyorsunuz.

Bir de pek anlatılmayan bir avantajı var: kodun okunuşunda hata yolları ve başarı yolu açıkça ayrışıyor. Herhangi bir satırda “bu çağrı başarısız olursa ne olur?” sorusunun cevabı hemen bir satır aşağıda görünüyor. Derin bir exception hiyerarşisi izlemek yerine, doğrusal bir akışı takip ediyorsunuz.

Dilin bu yaklaşımı benimsemesinin bedeli söz dizimi (syntax) düzeyinde tekrar, kazancı ise hata akışının şeffaflığı. Bu trade-off size uymuyorsa Go size uymayabilir — ama projeye girmeye değer bir dürüstlük bu.

Etiketler: #Go
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi