go
Go dersleri
Go/Tipler ve Soyutlama

Hata Yönetimi

error arayüzü, hata sarmalama, errors.Is/As, panic ve recover.

Ders 11 / 2125 dkOrta
Bu derste öğreneceklerin
  • error arayüzü ve if err != nil deyimi
  • errors.New ve fmt.Errorf
  • %w ile hata sarmalama
  • errors.Is ve errors.As
  • Sentinel hatalar ve özel hata tipleri
  • errors.Join
  • panic, recover ve ne zaman kullanılmalı

Bir kargo şirketiyle paket gönderdiğini düşün. Kurye adresi bulamazsa paketi ortada bırakıp ortadan kaybolmaz; geri döner ve sana bir not verir: "Adres bulunamadı, bina numarası eksik." Bu notla ne yapacağına sen karar verirsin: Adresi düzeltip yeniden gönderirsin, alıcıyı ararsın ya da siparişi iptal edersin. Go'da hatalar da böyle çalışır. Bir işlem başarısız olduğunda fonksiyon sıradan bir dönüş değeri olarak bir hata döndürür ve bununla ne yapılacağına çağıran taraf karar verir.

Birçok dil hataları istisnalarla (exceptions) yönetir: Bir yerde fırlatılan istisna, aradaki fonksiyonların hiçbirinde görünmeden çağrı yığınında yukarı doğru uçar ve bir catch bloğunda yakalanır. Go bilinçli olarak başka bir yol seçti. Hatalar değerdir (errors are values): Bir değişkende saklanabilir, karşılaştırılabilir, başka bir hatanın içine sarılabilir ve birleştirilebilir. Bunun bedeli sık sık if err != nil yazmaktır; kazancı ise her hatanın nerede ortaya çıktığını ve nasıl ele alındığını yalnızca kodu okuyarak görebilmektir.

Bu derste error arayüzünden başlayıp errors.New ve fmt.Errorf ile hata üretmeyi, %w ile hataları katman katman sarmalamayı ve errors.Is ile errors.As kullanarak bu zinciri sorgulamayı öğreneceksin. Sentinel hatalar ile özel hata tipleri arasında nasıl seçim yapacağını, errors.Join ile birden fazla hatayı nasıl birleştireceğini göreceksin. Son olarak panic ve recover'ın gerçekte ne için var olduğunu, panikleri sistem sınırlarında nasıl hataya çevireceğini ve os.Exit'in neden defer'ları atladığını konuşacağız.

Hatalar birer değerdir: error arayüzü

Go'da hata, dilin yerleşik (built-in) ve son derece küçük bir arayüzüdür:

type error interface {
	Error() string
}

Error() string metoduna sahip her tip bir hatadır. Arayüzler dersinde gördüğün örtük uygulama sayesinde kendi hata tiplerini yazmak da bu kadar kolaydır. Başarısız olabilen fonksiyonlar geleneksel olarak error'ı son dönüş değeri olarak döndürür. Hata nil ise işlem başarılıdır.

if err != nil deyimi

Standart kütüphanedeki strconv.Atoi bir metni tamsayıya çevirir ve çeviremezse hata döndürür:

main.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	inputs := []string{"42", "on iki", "-7"}

	for _, s := range inputs {
		n, err := strconv.Atoi(s)
		if err != nil {
			fmt.Println("hata:", err)
			continue
		}
		fmt.Println("sayı:", n)
	}
}
Çıktı
sayı: 42
hata: strconv.Atoi: parsing "on iki": invalid syntax
sayı: -7

n, err := ... ve hemen ardından gelen if err != nil Go kodunun belki de en tanıdık kalıbıdır. Bu kalıbın arkasında birkaç yerleşik gelenek var:

  • Hatayı çağrıdan hemen sonra kontrol et. Kontrolü ertelemek, hatalı bir değerle çalışmaya devam etmek demektir.
  • Hata nil değilse diğer dönüş değerlerine güvenme. Fonksiyonlar hata durumunda genellikle sıfır değer döndürür. Aksi belgelenmişse istisnadır: io.Reader'ın Read metodu hata ile birlikte okunmuş byte sayısını da döndürebilir.
  • Hatayla bir şey yap. Ya ele al (yeniden dene, varsayılan değere düş, kullanıcıya anlaşılır bir mesaj göster) ya da bağlam ekleyip çağırana ilet. Sessizce yutmak neredeyse her zaman yanlıştır.

Bu yaklaşımın istisnalara göre en büyük farkı, kontrol akışının görünür olmasıdır. Bir fonksiyonun hata döndürüp döndüremeyeceğini imzasına bakarak anlarsın ve hatanın nerede ele alındığını bulmak için çağrı yığınında gizli catch bloklarını aramazsın.

errors.New ve fmt.Errorf

Kendi hatalarını üretmenin iki temel yolu var. errors.New sabit bir mesajla basit bir hata oluşturur. fmt.Errorf ise fmt.Sprintf gibi biçimlendirme fiilleriyle dinamik bir mesaj kurar:

main.go
package main

import (
	"errors"
	"fmt"
)

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

func withdraw(balance, amount int) (int, error) {
	if amount > balance {
		return balance, fmt.Errorf("yetersiz bakiye: %d TL istendi, %d TL var", amount, balance)
	}
	return balance - amount, nil
}

func main() {
	if _, err := divide(10, 0); err != nil {
		fmt.Println(err)
	}

	if _, err := withdraw(100, 250); err != nil {
		fmt.Println(err)
	}

	e1 := errors.New("aynı mesaj")
	e2 := errors.New("aynı mesaj")
	fmt.Println(e1 == e2, e1 == e1)
}
Çıktı
sıfıra bölme
yetersiz bakiye: 250 TL istendi, 100 TL var
false true

Son satır önemli bir ayrıntıyı gösteriyor: errors.New her çağrıldığında içeride yeni bir işaretçi döndürür. Mesajları aynı olsa bile iki ayrı errors.New çağrısının sonucu birbirine eşit değildir. Hataların eşitliği metinlerine değil kimliklerine bağlıdır. Birazdan göreceğin sentinel hatalar tam olarak bu özelliğe dayanır.

Hata mesajları için Go'da yaygın kabul görmüş bir yazım kuralı vardır: Küçük harfle başla ve noktalama işaretiyle bitirme. Çünkü hatalar katmanlar boyunca birbirine eklenir ve sonunda tek bir satırda okunur: sunucu başlatılamadı: yapılandırma okunamadı: dosya bulunamadı. Ortada büyük harfle başlayan veya nokta ile biten bir parça bu cümleyi bozar.

%w ile hata sarmalama

Gerçek programlar katmanlıdır. Bir HTTP isteği bir servis fonksiyonunu, o da bir veri erişim fonksiyonunu çağırır. En alttaki fonksiyonun ürettiği "kayıt bulunamadı" mesajı en üste olduğu gibi çıkarsa, günlük kayıtlarında (log) bu mesajı gördüğünde hangi isteğin, hangi kullanıcının kaydını aradığını bilemezsin. Her katman bildiği bağlamı hataya eklemelidir.

Bunu fmt.Errorf'un özel %w fiiliyle yaparsın. %w, %v gibi hatanın mesajını metne ekler; ama ayrıca orijinal hatayı yeni hatanın içinde saklar. Buna hata sarmalama (wrapping) denir. Sarmalanan hataya errors.Unwrap ile geri ulaşılabilir:

main.go
package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("kayıt bulunamadı")

// Veri katmanı
func findUser(id int) (string, error) {
	if id != 1 {
		return "", ErrNotFound
	}
	return "Ayşe", nil
}

// Servis katmanı
func loadProfile(id int) (string, error) {
	name, err := findUser(id)
	if err != nil {
		return "", fmt.Errorf("kullanıcı %d yüklenemedi: %w", id, err)
	}
	return "Profil: " + name, nil
}

// HTTP katmanı
func handleRequest(id int) error {
	if _, err := loadProfile(id); err != nil {
		return fmt.Errorf("GET /users/%d: %w", id, err)
	}
	return nil
}

func main() {
	err := handleRequest(42)
	fmt.Println(err)

	fmt.Println("zincir:")
	for e := err; e != nil; e = errors.Unwrap(e) {
		fmt.Println(" ->", e)
	}

	flat := fmt.Errorf("kullanıcı 42 yüklenemedi: %v", ErrNotFound)
	fmt.Println("v fiiliyle sarılan:", errors.Unwrap(flat))
}
Çıktı
GET /users/42: kullanıcı 42 yüklenemedi: kayıt bulunamadı
zincir:
 -> GET /users/42: kullanıcı 42 yüklenemedi: kayıt bulunamadı
 -> kullanıcı 42 yüklenemedi: kayıt bulunamadı
 -> kayıt bulunamadı
v fiiliyle sarılan: <nil>

Her katman yalnızca kendi bildiğini ekledi: HTTP katmanı isteği, servis katmanı kullanıcı kimliğini. Ortaya çıkan hata, içinde bir öncekini taşıyan bir zincirdir:

  handleRequest'in döndürdüğü hata
  "GET /users/42: kullanıcı 42 yüklenemedi: kayıt bulunamadı"
        |
        | Unwrap()
        v
  loadProfile'ın döndürdüğü hata
  "kullanıcı 42 yüklenemedi: kayıt bulunamadı"
        |
        | Unwrap()
        v
  ErrNotFound
  "kayıt bulunamadı"
        |
        | Unwrap()
        v
       nil

Teknik olarak fmt.Errorf bir %w fiili gördüğünde Unwrap() error metoduna sahip bir hata değeri döndürür; errors.Unwrap de bu metodu çağırır. Programın son satırındaki %v ise yalnızca metni kopyaladı: Mesaj tamamen aynı görünse de içeride orijinal hata yok ve Unwrap nil döndürüyor.

FiilMesajOrijinal hata saklanır mı?errors.Is ve errors.As görür mü?
%vHatanın metni eklenirHayırHayır
%wHatanın metni eklenirEvet, Unwrap ile erişilirEvet

Peki her zaman %w mi kullanmalısın? Hayır. Sarmaladığın hata, paketinin dışarıya sunduğu sözleşmenin (API) bir parçası hâline gelir: Çağıranlar içerideki hataya bakarak karar vermeye başlar. Örneğin bir depolama paketi içeride hangi veritabanı sürücüsünü kullandığını gizlemek istiyorsa, sürücünün hatalarını %w ile değil %v ile ekler ve dışarıya kendi sentinel hatalarını sunar. Çağıranın içerideki hatayı incelemesini istediğinde %w, bunu bir uygulama ayrıntısı olarak saklamak istediğinde %v kullan.

Sentinel hatalar ve errors.Is

Sentinel (nöbetçi) hata, paket düzeyinde tanımlanıp dışa aktarılan ve belirli bir durumu temsil eden hata değişkenidir. Adları geleneksel olarak Err ile başlar. Standart kütüphanede pek çok örneği var: io.EOF, fs.ErrNotExist, sql.ErrNoRows, context.Canceled.

Çağıran taraf bir sentinel hatayı tanımak için errors.Is(err, hedef) kullanır. errors.Is, hatanın kendisinden başlayarak Unwrap zincirinde yürür ve her adımda hedef ile eşitliği kontrol eder:

main.go
package main

import (
	"errors"
	"fmt"
)

var (
	ErrNotFound     = errors.New("ürün bulunamadı")
	ErrUnauthorized = errors.New("yetkisiz erişim")
)

var stock = map[string]int{"elma": 10, "armut": 0}

func buy(user, item string) error {
	if user == "" {
		return ErrUnauthorized
	}
	count, ok := stock[item]
	if !ok {
		return fmt.Errorf("%q: %w", item, ErrNotFound)
	}
	if count == 0 {
		return fmt.Errorf("%q stokta yok", item)
	}
	stock[item]--
	return nil
}

func main() {
	attempts := []struct{ user, item string }{
		{"ayse", "elma"},
		{"ayse", "kiraz"},
		{"", "elma"},
		{"ayse", "armut"},
	}

	for _, a := range attempts {
		err := buy(a.user, a.item)
		switch {
		case err == nil:
			fmt.Println("satın alındı:", a.item)
		case errors.Is(err, ErrNotFound):
			fmt.Println("bulunamadı ->", err)
		case errors.Is(err, ErrUnauthorized):
			fmt.Println("giriş yapmalısın ->", err)
		default:
			fmt.Println("beklenmeyen hata ->", err)
		}
	}

	wrapped := fmt.Errorf("sipariş: %w", ErrNotFound)
	fmt.Println(wrapped == ErrNotFound, errors.Is(wrapped, ErrNotFound))
}
Çıktı
satın alındı: elma
bulunamadı -> "kiraz": ürün bulunamadı
giriş yapmalısın -> yetkisiz erişim
beklenmeyen hata -> "armut" stokta yok
false true

Son satır, sarmalanmış hatalarda == kullanmanın neden işe yaramadığını gösteriyor: wrapped artık ErrNotFound'un kendisi değil, onu içinde taşıyan başka bir değer. errors.Is ise zinciri takip ederek içerideki ErrNotFound'u buluyor. switch bloğu da gerçek programlarda sık görülen bir deseni gösteriyor: Hatanın türüne göre farklı bir yanıt üretmek (örneğin bir HTTP sunucusunda 404, 401 veya 500 döndürmek).

Sentinel hataların iki sınırlılığı var. Birincisi, paketinin API'sinin bir parçası olurlar; bir kez dışa aktardığın sentinel'i kaldırmak çağıranların kodunu bozar. İkincisi ve daha önemlisi, ek bilgi taşıyamazlar: ErrNotFound hangi kaydın bulunamadığını söyleyemez. Hatayla birlikte yapılandırılmış veri taşıman gerekiyorsa özel bir hata tipi yazman gerekir.

Özel hata tipleri ve errors.As

Error() string metoduna sahip her tip bir hata olabildiğine göre, alanları olan bir struct da hata olabilir. Çağıran bu alanlara ulaşmak için errors.As kullanır: errors.As(err, &hedef) zincirde hedef'in tipine atanabilen ilk hatayı bulur, onu hedef'e yazar ve true döndürür.

main.go
package main

import (
	"errors"
	"fmt"
)

// ValidationError, hangi alanın neden geçersiz olduğunu taşır.
type ValidationError struct {
	Field  string
	Reason string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("%s alanı geçersiz: %s", e.Field, e.Reason)
}

type User struct {
	Name string
	Age  int
}

func validate(u User) error {
	if u.Name == "" {
		return &ValidationError{Field: "name", Reason: "boş olamaz"}
	}
	if u.Age < 18 {
		return &ValidationError{Field: "age", Reason: fmt.Sprintf("%d, 18'den küçük", u.Age)}
	}
	return nil
}

func register(u User) error {
	if err := validate(u); err != nil {
		return fmt.Errorf("kayıt başarısız: %w", err)
	}
	return nil
}

func main() {
	err := register(User{Name: "Can", Age: 15})
	fmt.Println(err)

	var ve *ValidationError
	if errors.As(err, &ve) {
		fmt.Println("alan:", ve.Field)
		fmt.Println("neden:", ve.Reason)
	}

	// Go 1.26: errors.AsType aynı işi jenerik bir fonksiyonla yapar.
	if ve, ok := errors.AsType[*ValidationError](err); ok {
		fmt.Println("AsType ile alan:", ve.Field)
	}
}
Çıktı
kayıt başarısız: age alanı geçersiz: 15, 18'den küçük
alan: age
neden: 15, 18'den küçük
AsType ile alan: age

errors.As kullanımının en çok kafa karıştıran kısmı ikinci argümandır. Error metodu işaretçi alıcıyla tanımlandığı için zincirdeki hata *ValidationError tipindedir. Bu yüzden ve değişkenini *ValidationError olarak tanımlarız ve errors.As'e onun adresini, yani **ValidationError tipinde bir değer veririz. errors.As bulduğu hatayı bu adrese yazabilmek için işaretçiye ihtiyaç duyar. İşaretçi yerine değer verirsen errors.As panik oluşturur; neyse ki go vet bu hatayı derlemeden önce yakalar.

Go 1.26 ile gelen errors.AsType aynı aramayı tip parametresiyle yapar: Hedef değişkeni önceden tanımlaman gerekmez, sonuç ve ok doğrudan döner ve yanlış argüman verme riski ortadan kalkar. Tip parametrelerini Jenerikler dersinde ayrıntılı göreceksin.

Standart kütüphane de sentinel'leri ve özel tipleri birlikte kullanır. strconv.Atoi'nin döndürdüğü *strconv.NumError, hangi fonksiyonun hangi girdide başarısız olduğunu alanlarında taşır ve içinde strconv.ErrRange gibi bir sentinel'i sarmalar. Böylece aynı hataya hem errors.As hem errors.Is ile bakabilirsin:

main.go
package main

import (
	"errors"
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.Atoi("99999999999999999999")
	fmt.Println(err)

	if numErr, ok := errors.AsType[*strconv.NumError](err); ok {
		fmt.Printf("fonksiyon: %s, girdi: %s, neden: %v\n", numErr.Func, numErr.Num, numErr.Err)
	}
	fmt.Println("aralık hatası mı:", errors.Is(err, strconv.ErrRange))
	fmt.Println("sözdizimi hatası mı:", errors.Is(err, strconv.ErrSyntax))
}
Çıktı
strconv.Atoi: parsing "99999999999999999999": value out of range
fonksiyon: Atoi, girdi: 99999999999999999999, neden: value out of range
aralık hatası mı: true
sözdizimi hatası mı: false

Hangi yaklaşımı ne zaman seçeceğine dair kısa bir özet:

YaklaşımÖrnekÇağıran nasıl kontrol eder?Ne zaman?
Sentinel hataio.EOF, ErrNotFounderrors.IsEk veri gerektirmeyen, iyi bilinen durumlar
Özel hata tipi*fs.PathError, *ValidationErrorerrors.As, errors.AsTypeHata yapılandırılmış bilgi taşımalıysa
Opak hatafmt.Errorf("...: %v", err)Yalnızca err != nilÇağıranın ayrıntıya bağımlı olmasını istemiyorsan

errors.Join ile hataları birleştirmek

Bazen tek bir işlem birden fazla bağımsız hata üretir. Bir form doğrulanırken kullanıcıya yalnızca ilk hatayı değil, bütün hatalı alanları göstermek istersin. Go 1.20 ile gelen errors.Join birden çok hatayı tek bir hata değerinde birleştirir:

main.go
package main

import (
	"errors"
	"fmt"
	"strings"
)

var (
	ErrEmpty    = errors.New("boş olamaz")
	ErrTooShort = errors.New("çok kısa")
)

type Form struct {
	Name     string
	Email    string
	Password string
}

func validateForm(f Form) error {
	var errs []error
	if f.Name == "" {
		errs = append(errs, fmt.Errorf("ad: %w", ErrEmpty))
	}
	if !strings.Contains(f.Email, "@") {
		errs = append(errs, fmt.Errorf("e-posta: %q geçerli bir adres değil", f.Email))
	}
	if len(f.Password) < 8 {
		errs = append(errs, fmt.Errorf("parola: %w", ErrTooShort))
	}
	return errors.Join(errs...) // errs boşsa nil döner
}

func main() {
	err := validateForm(Form{Email: "gopher.dev", Password: "123"})
	fmt.Println(err)
	fmt.Println("---")
	fmt.Println("boş alan var mı:", errors.Is(err, ErrEmpty))
	fmt.Println("kısa parola var mı:", errors.Is(err, ErrTooShort))

	err = validateForm(Form{Name: "Ada", Email: "[email protected]", Password: "gizli-parola"})
	fmt.Println("geçerli formun hatası:", err)
}
Çıktı
ad: boş olamaz
e-posta: "gopher.dev" geçerli bir adres değil
parola: çok kısa
---
boş alan var mı: true
kısa parola var mı: true
geçerli formun hatası: <nil>

errors.Join'in üç önemli özelliği var:

  • Kendisine verilen nil değerleri atlar; hepsi nil ise nil döndürür. Bu yüzden return errors.Join(errs...) satırı başarı durumunu da doğru şekilde ifade eder.
  • Birleşik hatanın mesajı, parçaların mesajlarının aralarına satır sonu konarak birleştirilmesiyle oluşur.
  • Birleşik hata Unwrap() []error metoduna sahiptir. errors.Is ve errors.As artık bir zincirde değil, bir ağaçta derinlik öncelikli olarak arama yapar.
                       errors.Join(...)
                /             |              \
     "ad: %w"        "e-posta: ... değil"      "parola: %w"
         |                                          |
     ErrEmpty                                  ErrTooShort

  errors.Is(err, ErrTooShort): kök -> "ad" -> ErrEmpty -> "e-posta" -> "parola" -> ErrTooShort (bulundu)

fmt.Errorf da Go 1.20'den beri birden fazla %w kabul eder: fmt.Errorf("kayıt: %w; bildirim: %w", err1, err2) her iki hatayı da ağaçta tutar.

Hata yönetimi desenleri

Erken dönüş: mutlu yol sola yaslı

Hata kontrollerini iç içe if-else bloklarıyla yazmak kodu hızla sağa doğru kaydırır ve asıl işi okumayı zorlaştırır. Go'da yerleşik alışkanlık, hatayı gördüğün anda dönmek ve başarılı akışı (mutlu yolu) sol kenarda tutmaktır:

func process(path string) error {
	f, err := os.Open(path)
	if err == nil {
		defer f.Close()
		data, err := io.ReadAll(f)
		if err == nil {
			if len(data) > 0 {
				return handle(data) // asıl iş üç seviye içeride
			} else {
				return errors.New("dosya boş")
			}
		} else {
			return err
		}
	} else {
		return err
	}
}
func process(path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	data, err := io.ReadAll(f)
	if err != nil {
		return fmt.Errorf("okuma: %w", err)
	}
	if len(data) == 0 {
		return errors.New("dosya boş")
	}

	return handle(data) // mutlu yol sol kenarda
}

İkinci sürümde her hata kontrolü kendi başına okunur ve fonksiyonun sonuna gelen okur, oraya kadar her şeyin yolunda gittiğini bilir. defer f.Close() satırının dosya başarıyla açıldıktan hemen sonra geldiğine de dikkat et; defer'ın ayrıntılarını Fonksiyonlar dersinde görmüştün.

Bağlam ekle, ama tekrarlama

Bir hatayı yukarı iletirken ekleyeceğin bağlam, hatayı okuyan kişinin "ne yapılırken, hangi girdiyle" sorusuna cevap vermelidir. İki tuzağa dikkat et:

  • Alttaki hatanın zaten söylediğini tekrarlama. os.Open hatası dosya yolunu zaten içerir: open config.json: no such file or directory. Bunu fmt.Errorf("config.json açılamadı: %w", err) ile sararsan yol iki kez görünür. fmt.Errorf("yapılandırma yükleme: %w", err) yeterlidir.
  • Anlamsız önekler ekleme. "hata oluştu:", "başarısız:" gibi parçalar her katmanda tekrarlanınca mesajı uzatır ama bilgi eklemez. Kısa ve olayı anlatan ifadeler kullan.

Ya logla ya döndür, ikisini birden yapma

Yeni başlayanların sık yaptığı bir hata, her katmanda hatayı hem loglayıp hem de yukarı döndürmektir. Sonuçta tek bir hata, günlük kayıtlarında üç farklı satır olarak görünür ve hangisinin asıl olay olduğunu anlamak zorlaşır:

dosya okunamadı: open config.json: no such file or directory
yapılandırma yüklenemedi: open config.json: no such file or directory
sunucu başlatılamadı: open config.json: no such file or directory

Kural basit: Bir hatayı ya ele al ya da ilet. Loglamak bir ele alma biçimidir ve genellikle en üst katmanda (main, HTTP handler, iş kuyruğunun döngüsü) bir kez yapılır. Alt katmanlar yalnızca bağlam ekleyip döndürür:

 func loadConfig(path string) (*Config, error) {
 	data, err := os.ReadFile(path)
 	if err != nil {
-		log.Printf("dosya okunamadı: %v", err)
-		return nil, err
+		return nil, fmt.Errorf("yapılandırma yükleme: %w", err)
 	}
 	return parseConfig(data)
 }

Bir hatayı bilerek yok saydığın durumlar da olabilir. Bu durumda bunu gizleme; _ = conn.Close() gibi açıkça yaz ve gerekiyorsa nedenini bir yorumla belirt. Kodu okuyan kişi hatanın unutulmadığını, bilinçli olarak göz ardı edildiğini anlar.

panic ve recover

panic: normal akışı durdurmak

panic, programın normal akışını durduran yerleşik bir fonksiyondur. Bir panik oluştuğunda mevcut fonksiyon hemen durur, çağrı yığınında yukarı doğru her fonksiyonun defer edilmiş çağrıları çalıştırılır ve panik hiçbir yerde yakalanmazsa program bir hata mesajı ve yığın izi yazdırarak 2 çıkış koduyla sonlanır. Çalışma zamanı da bazı hatalarda kendiliğinden panik oluşturur: nil işaretçiyi çözmek, dilim sınırlarını aşmak, nil map'e yazmak, tamsayıyı sıfıra bölmek, başarısız tek değerli tip iddiası.

main.go
package main

import "fmt"

func main() {
	defer fmt.Println("main: defer çalıştı")

	nums := []int{1, 2, 3}
	i := 5
	fmt.Println("erişiliyor...")
	fmt.Println(nums[i])
	fmt.Println("bu satır hiç çalışmaz")
}
Çıktı
erişiliyor...
main: defer çalıştı
panic: runtime error: index out of range [5] with length 3

goroutine 1 [running]:
main.main()
	/tmp/main.go:10 +0x...
exit status 2

Panik mesajından önce defer edilmiş satırın çalıştığına dikkat et. Bu, paniğe rağmen dosyaların kapatılabilmesini ve kilitlerin serbest bırakılabilmesini sağlar.

Ne zaman panic, ne zaman error?

Go'da panik, istisnaların yerine kullanılacak bir kontrol akışı aracı değildir. Temel ayrım şudur: Beklenebilir hatalar error, programcı hataları panic. Dosyanın bulunamaması, ağın kopması, kullanıcının geçersiz veri girmesi normal hayatın parçasıdır ve programın bunlara hazırlıklı olması gerekir. Bir fonksiyonun asla nil olmaması gereken bir argümanla çağrılması veya "imkânsız" bir durumun oluşması ise kodda bir hata olduğunu gösterir.

DurumDoğru araç
Dosya yok, ağ hatası, zaman aşımıerror
Kullanıcının girdiği veri geçersizerror
Kodun kendi değişmezi (invariant) bozuldu, "buraya asla gelinmemeli"panic
Paket içinde sabit olarak yazılmış bir regex veya şablon derlenemedipanic (regexp.MustCompile)
Program başlarken zorunlu bir kaynak yokmain'de hata yazdırıp çıkış

Standart kütüphanedeki Must önekli fonksiyonlar (regexp.MustCompile, template.Must) bu ayrımın güzel bir örneğidir. Genellikle paket düzeyinde, kaynak koda sabit olarak yazılmış bir girdiyle çağrılırlar; bu girdi hatalıysa programın hiç çalışmaması doğru davranıştır. Kullanıcıdan gelen bir regex için ise hata döndüren regexp.Compile kullanılır.

recover ile paniği yakalamak

recover, panik hâlindeki bir goroutine'in kontrolünü geri almayı sağlayan yerleşik fonksiyondur. Yalnızca doğrudan defer edilen bir fonksiyonun içinde çağrıldığında işe yarar: Panik varsa paniğin değerini döndürür ve paniği durdurur, yoksa nil döndürür. Paniği yakalanan fonksiyon normal şekilde döner. İsimlendirilmiş dönüş değerlerini defer içinde değiştirerek çağırana anlamlı bir sonuç iletebilirsin:

main.go
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("bölme başarısız: %v", r)
		}
	}()
	return a / b, nil
}

func main() {
	fmt.Println(safeDivide(10, 2))
	fmt.Println(safeDivide(1, 0))
	fmt.Println("program çalışmaya devam ediyor")
}
Çıktı
5 <nil>
0 bölme başarısız: runtime error: integer divide by zero
program çalışmaya devam ediyor

Adım adım: safeDivide(1, 0) çağrısında a / b panik oluşturur ve return satırı hiç tamamlanmaz. Fonksiyondan çıkılırken defer edilen anonim fonksiyon çalışır, recover() panik değerini alır ve panik durur. err isimlendirilmiş dönüş değeri olduğu için defer içinde ona yazılan değer çağırana ulaşır; result ise sıfır değerinde kalır.

Elbette bu örnek yalnızca mekanizmayı göstermek için. Gerçek kodda sıfıra bölme durumunu önceden if b == 0 ile kontrol edip hata döndürmek çok daha doğrudur. recover beklenebilir hataları yönetmek için değil, beklenmedik hataların tüm sistemi çökertmesini önlemek için vardır.

Sınırda panikleri hataya çevirmek

recover'ın en meşru kullanım alanı sistem sınırlarıdır: Bir HTTP sunucusunda tek bir isteğin, bir iş kuyruğunda tek bir işin veya bir eklenti çağrısının içindeki hata, bütün süreci çökertmemelidir. Bu sınırlarda paniği yakalayıp hataya çevirirsin ve sistemin geri kalanı çalışmaya devam eder:

main.go
package main

import (
	"errors"
	"fmt"
)

// runSafely, bir işi çalıştırır ve olası paniği hataya çevirir.
func runSafely(name string, job func() error) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%s: panik yakalandı: %v", name, r)
		}
	}()
	return job()
}

func main() {
	jobs := []struct {
		name string
		fn   func() error
	}{
		{"rapor", func() error { return nil }},
		{"e-posta", func() error { return errors.New("sunucuya bağlanılamadı") }},
		{"istatistik", func() error {
			var counts map[string]int
			counts["ziyaret"]++ // nil map'e yazma: panik
			return nil
		}},
		{"yedekleme", func() error { return nil }},
	}

	for _, j := range jobs {
		if err := runSafely(j.name, j.fn); err != nil {
			fmt.Println("HATA:", err)
			continue
		}
		fmt.Println("tamam:", j.name)
	}
}
Çıktı
tamam: rapor
HATA: sunucuya bağlanılamadı
HATA: istatistik: panik yakalandı: assignment to entry in nil map
tamam: yedekleme

"istatistik" işindeki hata paniğe dönüştü ama "yedekleme" işi yine de çalıştı. Standart kütüphanenin net/http sunucusu da aynı fikri uygular: Bir handler panik oluşturursa sunucu paniği yakalar, yığın izini günlüğe yazar ve yalnızca o isteğin bağlantısını kapatır.

Paniği yakalamak hatayı çözmez, sadece hasarı sınırlar. Panik bir programcı hatasına işaret ettiği için sınırda yakaladığın paniği mutlaka logla ve mümkünse runtime/debug.Stack() ile yığın izini de kaydet; aksi hâlde hatayı sessizce gizlemiş olursun.

os.Exit defer'ları atlar

os.Exit programı anında sonlandırır: Çağrı yığınındaki defer'lar çalışmaz, panikteki gibi bir çözülme (unwinding) olmaz. log.Fatal ailesi de mesajı yazdıktan sonra os.Exit(1) çağırır, dolayısıyla aynı etkiye sahiptir:

main.go
package main

import (
	"fmt"
	"os"
)

func main() {
	defer fmt.Println("temizlik yapıldı") // hiç çalışmayacak

	fmt.Println("kritik hata, çıkılıyor")
	os.Exit(1)
}
Çıktı
kritik hata, çıkılıyor

Bu yüzden dosya kapatma, geçici dosyaları silme, tamponları diske yazma gibi temizlik işlerini defer ile yapıyorsan main içinde doğrudan os.Exit veya log.Fatal çağırmaktan kaçın. Yaygın çözüm, asıl işi hata döndüren bir run fonksiyonuna taşımak ve os.Exit'i yalnızca main'in en sonunda çağırmaktır:

main.go
package main

import (
	"errors"
	"fmt"
	"os"
)

func run() error {
	defer fmt.Println("run: kaynaklar kapatıldı")

	fmt.Println("run: çalışıyor")
	return errors.New("yapılandırma dosyası bulunamadı")
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "hata:", err)
		os.Exit(1)
	}
}
Çıktı
run: çalışıyor
run: kaynaklar kapatıldı
hata: yapılandırma dosyası bulunamadı

run döndüğünde defer'ları çoktan çalışmış olur; os.Exit artık hiçbir temizliği atlamaz. Bu desen main'i ince tutar ve run'ı test etmeyi de kolaylaştırır.

Sık yapılan hatalar

  • Hatayı _ ile sessizce yutmak. n, _ := strconv.Atoi(s) geçersiz girdide 0 ile devam eder ve hata çok daha sonra, alakasız bir yerde ortaya çıkar.
  • Sarmalanmış hataları == ile karşılaştırmak. Hata bir kez %w ile sarıldığında err == ErrNotFound false olur. errors.Is kullan.
  • %v ile sarıp errors.Is beklemek. %v yalnızca metni kopyalar; zincir kopar. Çağıranın inceleyebilmesini istiyorsan %w kullan.
  • Hem loglayıp hem döndürmek. Aynı hata günlükte defalarca görünür. Alt katmanlarda bağlam ekleyip döndür, en üstte bir kez logla.
  • nil işaretçi tipli hata döndürmek. var err *MyError; return err nil olmayan bir error üretir. Ayrıntılar Arayüzler dersindeki nil arayüz tuzağında.
  • errors.As'e yanlış hedef vermek. Hedef bir işaretçi olmalı ve tipi zincirdeki hatanın tipiyle eşleşmeli: Error işaretçi alıcıyla tanımlandıysa var ve *ValidationError ve errors.As(err, &ve). Değer tipiyle arama yaparsan eşleşme sessizce başarısız olur.
  • Hata mesajını büyük harfle başlatıp noktayla bitirmek. Birleşen mesajlarda cümle ortasında büyük harf ve nokta oluşur.
  • Beklenebilir durumlar için panik kullanmak. Dosya bulunamadı veya geçersiz girdi gibi durumlar error'dur. Panik programcı hataları içindir.
  • log.Fatal veya os.Exit sonrasında defer'ların çalışacağını sanmak. Çalışmazlar; temizlik gerektiren işleri run() error deseniyle yapılandır.
  • Hata metnine göre karar vermek. strings.Contains(err.Error(), "not found") mesaj değiştiği anda bozulur; sentinel veya özel tip kullan.

Alıştırmalar

Alıştırma·Yaşı ayrıştır ve hatayı sınıflandır
Kolay

parseAge(s string) (int, error) fonksiyonunu yaz:

  • strconv.Atoi başarısız olursa hatayı bağlam ekleyerek %w ile sarmala.
  • Sayı 0 ile 150 arasında değilse paket düzeyinde tanımladığın ErrOutOfRange sentinel hatasını, sayıyı da içeren bir mesajla sarmalayarak döndür.

main içinde "34", "otuz", "-5" ve "200" girdilerini dene. Hataları errors.Is ile sınıflandır: sayı olmayan girdiler için strconv.ErrSyntax, aralık dışı değerler için ErrOutOfRange.

İpucu

strconv.Atoi'nin döndürdüğü hata içeride strconv.ErrSyntax'ı sarmalar; sen de onu %w ile sararsan zincir korunur. Alttaki hata girdiyi zaten içerdiği için mesajına girdiyi yeniden eklemene gerek yok.

Çözümü göster
main.go
package main

import (
	"errors"
	"fmt"
	"strconv"
)

var ErrOutOfRange = errors.New("geçerli aralığın dışında")

func parseAge(s string) (int, error) {
	age, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("yaş okunamadı: %w", err)
	}
	if age < 0 || age > 150 {
		return 0, fmt.Errorf("yaş %d: %w", age, ErrOutOfRange)
	}
	return age, nil
}

func main() {
	for _, in := range []string{"34", "otuz", "-5", "200"} {
		age, err := parseAge(in)
		switch {
		case err == nil:
			fmt.Println("geçerli yaş:", age)
		case errors.Is(err, strconv.ErrSyntax):
			fmt.Println("sayı değil ->", err)
		case errors.Is(err, ErrOutOfRange):
			fmt.Println("aralık dışı ->", err)
		default:
			fmt.Println("beklenmeyen ->", err)
		}
	}
}
Çıktı
geçerli yaş: 34
sayı değil -> yaş okunamadı: strconv.Atoi: parsing "otuz": invalid syntax
aralık dışı -> yaş -5: geçerli aralığın dışında
aralık dışı -> yaş 200: geçerli aralığın dışında

"otuz" girdisinde zincir üç halkalıdır: bizim fmt.Errorf sarmalayıcımız, *strconv.NumError ve onun içindeki strconv.ErrSyntax. errors.Is bu zinciri sonuna kadar takip ettiği için en içteki sentinel'i bulur. Sayısal değer için %w ile eklenen ErrOutOfRange, mesaja bağlam katarken çağıranın hatayı tanımasını da mümkün kılar.

Alıştırma·HTTP hatası ve yeniden deneme kararı
Orta

Durum kodunu ve adresi taşıyan bir HTTPError tipi yaz. fetch(url string) error fonksiyonu, aşağıdaki sahte yanıt tablosuna göre 400 ve üzeri kodlar için *HTTPError döndürsün; tabloda olmayan adresler 404 sayılsın. syncData(url string) error fonksiyonu fetch hatasını bağlam ekleyerek sarmalasın.

Son olarak retryable(err error) bool fonksiyonunu yaz: Zincirde bir *HTTPError varsa ve durum kodu 500 veya üzerindeyse true döndürsün.

var responses = map[string]int{
	"/users":   200,
	"/orders":  503,
	"/admin":   403,
	"/reports": 500,
}
İpucu

retryable içinde var httpErr *HTTPError tanımlayıp errors.As(err, &httpErr) çağır. Hata sarmalanmış olsa da errors.As onu bulur. Nil bir hata için errors.As false döndürür.

Çözümü göster
main.go
package main

import (
	"errors"
	"fmt"
)

type HTTPError struct {
	StatusCode int
	URL        string
}

func (e *HTTPError) Error() string {
	return fmt.Sprintf("%s: HTTP %d", e.URL, e.StatusCode)
}

// Sahte sunucu yanıtları
var responses = map[string]int{
	"/users":   200,
	"/orders":  503,
	"/admin":   403,
	"/reports": 500,
}

func fetch(url string) error {
	code, ok := responses[url]
	if !ok {
		code = 404
	}
	if code >= 400 {
		return &HTTPError{StatusCode: code, URL: url}
	}
	return nil
}

func syncData(url string) error {
	if err := fetch(url); err != nil {
		return fmt.Errorf("senkronizasyon: %w", err)
	}
	return nil
}

// retryable, hatanın yeniden denemeye değer olup olmadığını söyler.
func retryable(err error) bool {
	var httpErr *HTTPError
	return errors.As(err, &httpErr) && httpErr.StatusCode >= 500
}

func main() {
	for _, url := range []string{"/users", "/orders", "/admin", "/reports", "/yok"} {
		err := syncData(url)
		if err == nil {
			fmt.Println(url, "başarılı")
			continue
		}
		fmt.Printf("%-35s yeniden dene: %t\n", err, retryable(err))
	}
}
Çıktı
/users başarılı
senkronizasyon: /orders: HTTP 503   yeniden dene: true
senkronizasyon: /admin: HTTP 403    yeniden dene: false
senkronizasyon: /reports: HTTP 500  yeniden dene: true
senkronizasyon: /yok: HTTP 404      yeniden dene: false

retryable hatanın mesajına değil tipine ve alanına bakıyor; mesaj biçimi değişse de karar bozulmaz. && kısa devre yaptığı için errors.As false döndürdüğünde nil olan httpErr'e hiç erişilmez. Aynı fonksiyon Go 1.26'da httpErr, ok := errors.AsType[*HTTPError](err) ile hedef değişken tanımlamadan da yazılabilir. fetch hata olmadığında açıkça nil döndürdüğü için nil arayüz tuzağına da düşülmüyor.

Alıştırma·Birleşik hatadaki bütün alan hatalarını topla
Zor

Aşağıdaki validate fonksiyonu hatalarını errors.Join ile birleştiriyor ve bazı alan hatalarını ayrıca fmt.Errorf ile sarmalıyor. errors.As zincirdeki ilk eşleşmeyi bulur; ama biz kullanıcıya bütün hatalı alanları göstermek istiyoruz.

Hata ağacında gezinip içindeki bütün *FieldError değerlerini sırasıyla toplayan fieldErrors(err error) []*FieldError fonksiyonunu yaz.

type FieldError struct {
	Field string
	Msg   string
}

func (e *FieldError) Error() string { return e.Field + ": " + e.Msg }

func validate(name, email string, age int) error {
	var errs []error
	if name == "" {
		errs = append(errs, &FieldError{"name", "boş olamaz"})
	}
	if email == "" {
		errs = append(errs, &FieldError{"email", "boş olamaz"})
	}
	if age < 18 {
		errs = append(errs, fmt.Errorf("yaş kontrolü: %w", &FieldError{"age", "18'den küçük"}))
	}
	return errors.Join(errs...)
}
İpucu

Özyinelemeli düşün. Her düğümde önce düğümün kendisinin *FieldError olup olmadığına tip iddiasıyla bak. Sonra düğümün çocuklarına in: Tip switch'te interface{ Unwrap() error } ve interface{ Unwrap() []error } gibi isimsiz arayüzleri case olarak kullanabilirsin.

Çözümü göster
main.go
package main

import (
	"errors"
	"fmt"
)

type FieldError struct {
	Field string
	Msg   string
}

func (e *FieldError) Error() string { return e.Field + ": " + e.Msg }

func validate(name, email string, age int) error {
	var errs []error
	if name == "" {
		errs = append(errs, &FieldError{"name", "boş olamaz"})
	}
	if email == "" {
		errs = append(errs, &FieldError{"email", "boş olamaz"})
	}
	if age < 18 {
		errs = append(errs, fmt.Errorf("yaş kontrolü: %w", &FieldError{"age", "18'den küçük"}))
	}
	return errors.Join(errs...)
}

// fieldErrors, hata ağacındaki tüm *FieldError değerlerini sırasıyla toplar.
func fieldErrors(err error) []*FieldError {
	if err == nil {
		return nil
	}
	var result []*FieldError
	if fe, ok := err.(*FieldError); ok {
		result = append(result, fe)
	}
	switch x := err.(type) {
	case interface{ Unwrap() error }:
		result = append(result, fieldErrors(x.Unwrap())...)
	case interface{ Unwrap() []error }:
		for _, child := range x.Unwrap() {
			result = append(result, fieldErrors(child)...)
		}
	}
	return result
}

func main() {
	err := validate("", "", 16)

	var first *FieldError
	if errors.As(err, &first) {
		fmt.Println("errors.As yalnızca ilkini bulur:", first.Field)
	}

	for _, fe := range fieldErrors(err) {
		fmt.Printf("alan=%s mesaj=%s\n", fe.Field, fe.Msg)
	}

	fmt.Println("hatasız form:", fieldErrors(validate("Ada", "[email protected]", 30)))
}
Çıktı
errors.As yalnızca ilkini bulur: name
alan=name mesaj=boş olamaz
alan=email mesaj=boş olamaz
alan=age mesaj=18'den küçük
hatasız form: []

Fonksiyon, errors.Is ve errors.As'in içeride yaptığı ön sıralı (pre-order) derinlik öncelikli gezinmenin aynısını yapıyor, ama ilk eşleşmede durmak yerine hepsini topluyor. errors.Join'in döndürdüğü değer Unwrap() []error case'ine, fmt.Errorf ile tek %w kullanılarak oluşturulan sarmalayıcı ise Unwrap() error case'ine düşer. Ağaçtaki her düğüm bir kez ziyaret edildiği için karmaşıklık, düğüm sayısı n olmak üzere O(n)'dir. Hatasız formda validate nil döndürdüğünden fonksiyon ilk satırda nil dilim döndürür.

Alıştırma·Adımları geri alan işlem
Zor

Bir sipariş işlemini adımlara böldüğünü düşün: stok ayır, ödeme al, kargo oluştur. Her adımın bir Do ve bir Undo fonksiyonu var. runSteps(steps []Step) error fonksiyonunu yaz:

  • Adımları sırayla çalıştırsın.
  • Bir adım hata döndürürse veya panik oluşturursa, o ana kadar başarıyla tamamlanmış adımları ters sırada geri alsın.
  • Döndürülen hata; asıl hatayı ve geri alma sırasında oluşan hataları errors.Join ile birlikte taşısın.
  • Bütün adımlar başarılıysa nil döndürsün.
type Step struct {
	Name string
	Do   func() error
	Undo func() error
}
İpucu

İsimlendirilmiş bir err dönüş değeri kullan ve bütün geri alma mantığını tek bir defer içine koy. defer içinde önce recover() ile paniği hataya çevir, sonra err nil değilse tamamlanan adım sayısını tutan sayaçtan geriye doğru Undo çağır.

Çözümü göster
main.go
package main

import (
	"errors"
	"fmt"
)

type Step struct {
	Name string
	Do   func() error
	Undo func() error
}

// runSteps adımları sırayla çalıştırır. Bir adım hata verir veya panik
// oluşturursa tamamlanmış adımları ters sırada geri alır.
func runSteps(steps []Step) (err error) {
	done := 0 // başarıyla tamamlanan adım sayısı

	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%s adımında panik: %v", steps[done].Name, r)
		}
		if err == nil {
			return
		}
		errs := []error{err}
		for i := done - 1; i >= 0; i-- {
			fmt.Println("geri alınıyor:", steps[i].Name)
			if undoErr := steps[i].Undo(); undoErr != nil {
				errs = append(errs, fmt.Errorf("%s geri alınamadı: %w", steps[i].Name, undoErr))
			}
		}
		err = errors.Join(errs...)
	}()

	for _, s := range steps {
		fmt.Println("çalışıyor:", s.Name)
		if stepErr := s.Do(); stepErr != nil {
			return fmt.Errorf("%s: %w", s.Name, stepErr)
		}
		done++
	}
	return nil
}

func main() {
	ok := func() error { return nil }
	steps := []Step{
		{Name: "stok ayır", Do: ok, Undo: ok},
		{Name: "ödeme al", Do: ok, Undo: func() error { return errors.New("banka yanıt vermiyor") }},
		{Name: "kargo oluştur", Do: func() error {
			var labels []string
			fmt.Println(labels[0]) // panik: dilim boş
			return nil
		}, Undo: ok},
	}

	fmt.Println("sonuç:", runSteps(steps[:2]))
	fmt.Println("===")

	err := runSteps(steps)
	fmt.Println("---")
	fmt.Println(err)
}
Çıktı
çalışıyor: stok ayır
çalışıyor: ödeme al
sonuç: <nil>
===
çalışıyor: stok ayır
çalışıyor: ödeme al
çalışıyor: kargo oluştur
geri alınıyor: ödeme al
geri alınıyor: stok ayır
---
kargo oluştur adımında panik: runtime error: index out of range [0] with length 0
ödeme al geri alınamadı: banka yanıt vermiyor

İlk çağrıda iki adım da başarılı olur ve defer içindeki err == nil kontrolü hiçbir şeyi geri almadan çıkar. İkinci çağrıda üçüncü adım panik oluşturur; done o anda 2 olduğu için steps[done] paniğin oluştuğu adımı gösterir. Geri alma döngüsü done - 1'den başlayarak yalnızca tamamlanmış adımları ters sırada gezer. "ödeme al" adımının geri alınamaması asıl hatayı gizlemez; errors.Join ikisini birlikte taşır ve çağıran errors.Is ile ikisini de sorgulayabilir. defer, recover ve isimlendirilmiş dönüş değeri üçlüsü, Go'da "ne olursa olsun temizlik yap" mantığının temel yapı taşıdır.

Kısa sınav

Kısa sınav

err := fmt.Errorf("okuma: %v", io.EOF) için errors.Is(err, io.EOF) ne döndürür?

func (e *ValidationError) Error() string tanımlıyken, sarmalanmış bir err içindeki doğrulama hatasına ulaşmak için hangisi doğrudur?

errors.Join(nil, nil) ne döndürür?

func f() (n int) { defer func() { recover(); n = -1 }(); panic("boom") } için fmt.Println(f()) ne yazdırır?

defer fmt.Println("A"), ardından fmt.Println("B") ve os.Exit(0) çalışırsa çıktı ne olur?

Aşağıdakilerden hangisi error döndürmek yerine panik için uygun bir durumdur?

Özet

  • Go'da hatalar Error() string metoduna sahip sıradan değerlerdir. Başarısız olabilen fonksiyonlar error'ı son dönüş değeri olarak döndürür ve çağıran if err != nil ile hemen kontrol eder.
  • errors.New sabit, fmt.Errorf biçimlendirilmiş hata üretir. Hata mesajları küçük harfle başlar ve noktalama ile bitmez.
  • %w hatayı bağlam ekleyerek sarmalar ve orijinalini korur; %v yalnızca metni kopyalar. Çağıranın içeriyi incelemesini istemiyorsan bilinçli olarak %v seç.
  • Sentinel hataları errors.Is ile, özel hata tiplerini errors.As veya Go 1.26'daki errors.AsType ile ara; sarmalanmış hataları asla == ile karşılaştırma.
  • errors.Join birden fazla hatayı bir ağaçta birleştirir, hepsi nil ise nil döndürür; errors.Is ve errors.As bu ağacın tamamında arama yapar.
  • Mutlu yolu sola yaslı tut, her katmanda tekrarsız bağlam ekle ve bir hatayı ya logla ya da döndür.
  • panic programcı hataları ve bozulan değişmezler içindir. recover yalnızca defer içinde ve aynı goroutine'de çalışır; en iyi kullanımı sistem sınırlarında panikleri hataya çevirmektir.
  • os.Exit ve log.Fatal defer'ları atlar; temizlik gerektiren programlarda run() error desenini kullan.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.