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 ve tanımı dilin spesifikasyonunda duruyor. Spesifikasyonun ifadesiyle error, “bir hata durumunu temsil etmek için kullanılan geleneksel interface”tir ve nil değeri “hata yok” demektir. 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)
fmt.Errorf ve Go 1.13 ile gelen %w verb, hata bağlamını zenginleştirmenin standart yolu haline geldi. Go ekibinin duyurusundaki tanımıyla: %w kullanıldığında fmt.Errorf’un döndürdüğü hata, %w’ye verilen argümanı geri veren bir Unwrap metoduna sahip olur.
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 — ilki hatayı bir değerle karşılaştırır, ikincisi belirli bir tipte olup olmadığını sınar; ikisi de zincirin tamamına bakar:
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. Aynı duyuru bunun için bir ölçüt de veriyor: hatayı çağırana incelemesi için açmak istiyorsanız sarmalayın, ama sarmalamak uygulama ayrıntısını dışarı sızdıracaksa sarmalamayın. 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, ¬Found) {
// 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.
Kaynaklar
Bu konudaki deneyler
Dile özgü serileştirme yerine dondurulmuş bir JSON zarfı; polyglot kuyruk standardı BabelQueue'ya dönüştü.
Şu an ne yapıyor
Dondurulmuş JSON zarfı dört dilde — PHP, Python, Go, Node.js — aynı baytları okutuyor; sidecar ya da broker eklentisi gerekmiyor. Polyglot kuyruk kuran ekipler spesifikasyonu ve SDK'ları bugün kullanabilir.
Diff'i makineden çıkarmadan bir modele denetleten, sağlayıcıdan bağımsız Go CLI; CommitBrief ürününe dönüştü.
Şu an ne yapıyor
Diff hiç makineden çıkmadan yerel kod incelemesi yapıyor; `Provider` arayüzü sayesinde Anthropic, OpenAI, Gemini ve Ollama aynı sözleşmeyle takılıyor. İmzalı binary'ler yayında — diff'ini bir bulut sağlayıcısına vermek istemeyen herkes kurabilir.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.