Golang Hata Yönetimi Şartnamesi

Hata Türleri

Hataları bildirmek için birkaç seçenek bulunmaktadır. Kullanım durumunuza en uygun seçeneği seçmeden önce aşağıdakileri göz önünde bulundurun:

  • Çağrının hatayı eşleştirmesi gerekli mi? Eğer öyleyse, errors.Is veya errors.As fonksiyonlarını desteklemek için üst düzey hata değişkenleri veya özel tipler belirtmeliyiz.
  • Hata iletileri statik bir dize mi yoksa bağlam bilgisi gerektiren dinamik bir dize mi? Statik dize için errors.New kullanabiliriz, ancak dinamik için fmt.Errorf veya özel bir hata türü kullanmalıyız.
  • Yeni hataları alt işlevlerden döndürüyorsak, hata sarmalamaya bakınız.
Hata Eşleşmesi? Hata İletisi Rehberlik
Hayır statik errors.New
Hayır dinamik fmt.Errorf
Evet statik errors.New ile üst düzey var
Evet dinamik özel error tipi

Örneğin, statik dize içeren hataları temsil etmek için errors.New kullanın. Çağrının bu hatayı eşleştirmesi ve ele alması gerekiyorsa, eşleşmeyi desteklemek için bir değişken olarak dışa aktarın.

Hata Eşleşmesi Yok

// foo paketi

func Open() error {
  return errors.New("açılamadı")
}

// bar paketi

if err := foo.Open(); err != nil {
  // Hata ele alınamıyor.
  panic("bilinmeyen hata")
}

Hata Eşleşmesi

// foo paketi

var ErrAçılamadı = errors.New("açılamadı")

func Open() error {
  return ErrAçılamadı
}

// bar paketi

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrAçılamadı) {
    // hatayı ele al
  } else {
    panic("bilinmeyen hata")
  }
}

Dinamik dizelerle ilgili hatalar için, çağrının eşleştirmesi gerekmiyorsa fmt.Errorf kullanın. Eğer çağrının gerçekten eşleştirmesi gerekiyorsa, özel bir error kullanın.

Hata Eşleşmesi Yok

// foo paketi

func Open(file string) error {
  return fmt.Errorf("%q dosyası bulunamadı", file)
}

// bar paketi

if err := foo.Open("testdosyası.txt"); err != nil {
  // Hata ele alınamıyor.
  panic("bilinmeyen hata")
}

Hata Eşleşmesi

// foo paketi

type BulunamadıHata struct {
  Dosya string
}

func (e *BulunamadıHata) Error() string {
  return fmt.Sprintf("%q dosyası bulunamadı", e.Dosya)
}

func Open(file string) error {
  return &BulunamadıHata{Dosya: file}
}

// bar paketi

if err := foo.Open("testdosyası.txt"); err != nil {
  var bulunamadı *BulunamadıHata
  if errors.As(err, &bulunamadı) {
    // hatayı ele al
  } else {
    panic("bilinmeyen hata")
  }
}

Unutmayın, bir paketten hata değişkenlerini veya tiplerini dışa aktarırsanız, bunlar paketin genel API'sinin bir parçası haline gelir.

Hata Kapsama

Başka bir yöntemi çağırırken bir hata oluştuğunda genellikle üç yöntem bulunur:

  • Orijinal hatayı olduğu gibi döndürün.
  • %w ile fmt.Errorf kullanarak hataya bağlam ekleyin ve ardından onu döndürün.
  • %v ile fmt.Errorf kullanarak hataya bağlam ekleyin ve ardından onu döndürün.

Eklemek için herhangi bir bağlam yoksa, orijinal hatayı olduğu gibi döndürün. Bu, orijinal hata türünü ve mesajını koruyacaktır. Bu özellikle alttaki hata iletiminden nereden kaynaklandığını izlemek için yeterli bilgi içeren hata mesajları olduğunda uygundur.

Aksi takdirde, belirsiz hataların ("bağlantı reddedildi" gibi) oluşmaması için mümkün olduğunca hata mesajına bağlam ekleyin. Bunun yerine "servis foo çağrısı: bağlantı reddedildi" gibi daha kullanışlı hatalar alacaksınız.

Hatalarınıza bağlam eklemek için fmt.Errorf kullanın ve %w veya %v fiilleri arasından seçim yaparak çağrıcının kök nedenini eşleştirebilmesine bağlı olarak tercih yapın.

  • Çağrıcının alttaki hataya erişim sağlaması gerekiyorsa %w kullanın. Bu, çoğu kapsama hatası için iyi bir varsayılan, ancak çağrıcının bu davranışa güvenmeye başlayabileceği konusunda farkında olun. Bu nedenle bilinen değişkenler veya tipler için kapsama hataları için onları fonksiyon sözleşmesinin bir parçası olarak kaydedin ve test edin.
  • Alttaki hatayı gizlemek için %v kullanın. Çağrıcı onu eşleştiremeyecek, ancak gerektiğinde %w'ye geçebilirsiniz.

Döndürülen hataya bağlam eklerken, bağlamı kısa tutmak için "başarısız oldu" gibi ifadelerden kaçının. Hata yığınında yayıldığında, katman katman yığılacak:

Tavsiye Edilmez:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store oluşturulurken hata oluştu: %w", err)
}

// x: y: new store oluşturulurken hata oluştu: the error

Tavsiye Edilen:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "yeni mağaza oluşturulurken: %w", err)
}
// x: y: yeni mağaza oluşturulurken: the error

Ancak, hata başka bir sisteme gönderildiğinde, iletişimin bir hatanın (örneğin, günlüklerde bir "err" etiketi veya "Hata" öneki) olduğu açık olmalıdır.

Yanlış Adlandırma

Global değişkenler olarak saklanan hata değerleri için, ihraç edilen olup olmadığına bağlı olarak Err veya err öneki kullanın. Lütfen talimatlarına bakın. İhraç edilmeyen üst düzey sabitler ve değişkenler için bir alt tire (_) öneki kullanın.

var (
  // Aşağıdaki iki hatayı ihraç edin, böylece bu paketin kullanıcıları errors.Is ile bunları eşleştirebilir.
  ErrBrokenLink = errors.New("bağlantı bozuk")
  ErrCouldNotOpen = errors.New("açılamadı")

  // Bu hata ihraç edilmez çünkü bunun genel API'nin bir parçası olmasını istemiyoruz. Yine de bunu paket içinde errors ile kullanabiliriz.
  errNotFound = errors.New("bulunamadı")
)

Özel hata tipleri için, Error soneki kullanın.

// Benzer şekilde, bu hata ihraç edilir, böylece bu paketin kullanıcıları errors.As ile bunu eşleştirebilir.
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("%q dosyası bulunamadı", e.File)
}

// Bu hata ihraç edilmez çünkü bunun genel API'nin bir parçası olmasını istemiyoruz. Yine de bunu paket içinde errors.As ile kullanabiliriz.
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("%q çözümlenemedi", e.Path)
}

Hata İşleme

Çağıran, çağrılan tarafından bir hata alındığında, hatanın anlaşılmasına bağlı olarak çeşitli şekillerde işlenebilir.

Bunlar arasında şunlar bulunur, ancak bunlarla sınırlı değildir:

  • Çağrılanın belirli bir hata tanımı üzerinde anlaştıysa hata için errors.Is veya errors.As ile eşleşme yapmak ve farklı yollarla dallanmayı işlemek
  • Hata günlüğüne kaydetmek ve hatanın kurtarılabilir ise zarif bir şekilde azaltmak
  • Bir alan özgü başarısızlık durumunu temsil ediyorsa, iyi tanımlanmış bir hata döndürmek
  • Sargılı veya doğrudan hatayı döndürmek

Çağrının hatayı nasıl işlediğine bakılmaksızın, genellikle her hatayı yalnızca bir kez işlemelidir. Örneğin, çağrılan hata günlüğüne kaydedip ardından geri döndürmemeli, çünkü kendisinin çağrısı da hatayı işleyebilir.

Örneğin, aşağıdaki senaryoları düşünün:

Kötü: Hata günlüğüne kaydetmek ve geri döndürmek

Yığın üzerindeki diğer çağrıcılar bu hatada benzer eylemler alabilir. Bu, uygulama günlüklerinde az bir fayda ile birçok gürültüye neden olur.

u, err := getUser(id)
if err != nil {
  // KÖTÜ: Açıklamaya bakın
  log.Printf("Kullanıcı alınamadı %q: %v", id, err)
  return err
}

İyi: Hata sarıp geri döndürmek

Yığının üstündeki hatalar bu hatayı işleyecektir. %w kullanmak, ilgili ise hataları errors.Is veya errors.As ile eşleştirebilmeyi sağlar.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("kullanıcı al %q: %w", id, err)
}

İyi: Hata günlüğüne kaydetmek ve zarif bir şekilde azaltmak

İşlem kesinlikle gerekli değilse, deneyimi kesintiye uğratmadan hatadan kurtularak zarif bir şekilde azaltma sağlayabiliriz.

if err := emitMetrics(); err != nil {
  // Metrik yazma başarısızlığı
  // uygulamayı bozmamalı.
  log.Printf("Metrikler yayınlanamadı: %v", err)
}

İyi: Hata eşleme ve uygun şekilde zarif bir şekilde azaltma

Çağrılan, anlaşmasında belirli bir hatayı tanımladıysa ve başarısızlık kurtarılabilirse, bu hata durumunu eşle ve zarif bir şekilde azalt. Diğer tüm durumlar için hatayı sarıp geri döndür. Yığının üstündeki hatalar diğer hataları işleyecektir.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // Kullanıcı mevcut değil. UTC kullan.
    tz = time.UTC
  } else {
    return fmt.Errorf("kullanıcı al %q: %w", id, err)
  }
}

Doğrulama Hatalarını İşleme

Tip doğrulamaları, yanlış tür tespiti durumunda tek bir geri dönüş değeri ile panikleyecektir. Bu nedenle her zaman "virgül, tamam" idiyomunu kullanın.

Tavsiye Edilmez:

t := i.(string)

Tavsiye Edilen:

t, ok := i.(string)
if !ok {
  // Hatası zarifçe işleyin
}

Panik kullanmaktan kaçının

Üretim ortamında çalışan kodlarda panik kullanmaktan kaçınılmalıdır. Panik, kaskad etkili hataların başlıca kaynağıdır. Bir hata oluştuğunda, işlev hatayı döndürmeli ve çağrıcıya bunun nasıl ele alınacağına karar verme imkanı tanımalıdır.

Tavsiye edilmez:

func run(args []string) {
  if len(args) == 0 {
    panic("bir argüman gereklidir")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Tavsiye edilen:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("bir argüman gereklidir")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Panik/yakala bir hata işleme stratejisi değildir. Yalnızca kurtarılamaz bir olay (örneğin, nil referansı) oluştuğunda panik olmalıdır. Bir istisna program başlatma aşamasında: programın paniklemesine neden olacak durumlar, program başlatılırken ele alınmalıdır.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Test kodlarında bile, hataların belirtilmesini sağlamak için panik yerine t.Fatal veya t.FailNow kullanmak tercih edilir.

Tavsiye edilmez:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("test kurulumu başarısız oldu")
}

Tavsiye edilen:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("test kurulumu başarısız oldu")
}