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:
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)
}
}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'ınReadmetodu 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:
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)
}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:
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))
}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
nilTeknik 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.
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:
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))
}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.
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)
}
}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:
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))
}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:
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:
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)
}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() []errormetoduna sahiptir.errors.Isveerrors.Asartı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.Openhatası dosya yolunu zaten içerir:open config.json: no such file or directory. Bunufmt.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 directoryKural 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ı.
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")
}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.
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:
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")
}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:
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)
}
}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:
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)
}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:
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)
}
}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%wile sarıldığındaerr == ErrNotFoundfalse olur.errors.Iskullan. %vile sarıperrors.Isbeklemek.%vyalnızca metni kopyalar; zincir kopar. Çağıranın inceleyebilmesini istiyorsan%wkullan.- 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 errnil olmayan birerrorü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:Errorişaretçi alıcıyla tanımlandıysavar ve *ValidationErrorveerrors.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.Fatalveyaos.Exitsonrasındadefer'ların çalışacağını sanmak. Çalışmazlar; temizlik gerektiren işlerirun() errordeseniyle 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
parseAge(s string) (int, error) fonksiyonunu yaz:
strconv.Atoibaşarısız olursa hatayı bağlam ekleyerek%wile sarmala.- Sayı 0 ile 150 arasında değilse paket düzeyinde tanımladığın
ErrOutOfRangesentinel 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
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)
}
}
}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.
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
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))
}
}/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.
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
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)))
}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.
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.Joinile 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
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)
}ç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
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() stringmetoduna sahip sıradan değerlerdir. Başarısız olabilen fonksiyonlarerror'ı son dönüş değeri olarak döndürür ve çağıranif err != nilile hemen kontrol eder. errors.Newsabit,fmt.Errorfbiçimlendirilmiş hata üretir. Hata mesajları küçük harfle başlar ve noktalama ile bitmez.%whatayı bağlam ekleyerek sarmalar ve orijinalini korur;%vyalnızca metni kopyalar. Çağıranın içeriyi incelemesini istemiyorsan bilinçli olarak%vseç.- Sentinel hataları
errors.Isile, özel hata tiplerinierrors.Asveya Go 1.26'dakierrors.AsTypeile ara; sarmalanmış hataları asla==ile karşılaştırma. errors.Joinbirden fazla hatayı bir ağaçta birleştirir, hepsi nil ise nil döndürür;errors.Isveerrors.Asbu 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.
panicprogramcı hataları ve bozulan değişmezler içindir.recoveryalnızcadeferiçinde ve aynı goroutine'de çalışır; en iyi kullanımı sistem sınırlarında panikleri hataya çevirmektir.os.Exitvelog.Fataldefer'ları atlar; temizlik gerektiren programlardarun() errordesenini kullan.