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
veyaerrors.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çinfmt.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
ilefmt.Errorf
kullanarak hataya bağlam ekleyin ve ardından onu döndürün. -
%v
ilefmt.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
veyaerrors.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")
}