Go'da Error Handling: error Değerleri, Wrapping, errors.Is/As/AsType ve panic
Go'da hata bir dönüş değeridir: sentinel error'lar errors.Is ile, custom error tipleri errors.As ya da Go 1.26'daki errors.AsType ile tanınır, %w ise zinciri koruyarak bağlam ekler. Bookstore'da ErrOutOfStock ve ValidationError, errors.Join ile toplu doğrulama, pointer error'ı %w ile sarma tuzağı ve onu yakalayan Go 1.27 vet kontrolü, hatayı 404'e çevirmek, handler'daki panic'in boş cevap üretmesi ve recover middleware'i.
Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; module'ün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenir. errors.AsType Go 1.26 ile gelmiştir. Pointer error'ı %w ile saran çağrıları yakalayan go vet kontrolü Go 1.27'ye özgüdür ve sadece go satırı 1.27 ya da üzeri olan dosyalarda çalışır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) sipariş kurallarına geçmeden önce küçük bir program. Bir doğrulama hatası tipi tanımlıyoruz, bir fonksiyon bu tipin pointer'ını %w ile sarıp döndürüyor, çağıran da errors.As ile hatanın bir ValidationError olup olmadığını soruyor:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string { return e.Field + ": " + e.Message }
func checkQuantity(q int) error {
if q <= 0 {
return fmt.Errorf("check quantity: %w", &ValidationError{Field: "quantity", Message: "must be positive"})
}
return nil
}
func main() {
err := checkQuantity(0)
var ve ValidationError
fmt.Println(err, errors.As(err, &ve))
}
Hata mesajında "quantity: must be positive" görüneceği belli. Soru, satırın sonundaki bool: errors.As hatayı bulur mu? İkinci soru: go vet bu kodda bir sorun görür mü? Module'ün go satırı 1.26.0 iken ve 1.27.0 iken ayrı ayrı düşün.
go run .
go vet . # go 1.26.0
go mod edit -go=1.27.0
go vet .
check quantity: quantity: must be positive false
main.go:17:38: %w wants operand of error type ValidationError, not pointer type *ValidationError (defeats errors.Is)
errors.As false döndürdü. Hata zincirinde bir *ValidationError var, biz ise ValidationError aradık; Go'da bunlar iki ayrı tip. Error() method'u value receiver'la tanımlandığı için ikisi de error interface'ini sağlıyor ve derleyici hiçbir şey demiyor. go vet, go satırı 1.26.0 iken sessiz kaldı; 1.27.0 olunca hatayı buldu. Go 1.27'nin vet'ine eklenen bu kontrol, dosyanın dil sürümüne bağlı. Bu yazıda Go'nun error handling yaklaşımını, Bookstore'un sipariş kurallarına sentinel error'lar, custom error tipleri ve wrapping ekleyerek göreceğiz.
Exception Yerine Dönüş Değeri
Go'da error tek method'lu bir interface: Error() string. Bir işlem başarısız olabiliyorsa son dönüş değeri error olur ve çağıran onu hemen kontrol eder. C# Temelleri serisinin "C# Exception Handling" yazısında anlattığımız try/catch, exception'ın call stack'te yukarı doğru kendiliğinden yürümesine dayanıyordu. Go'da böyle bir yürüme yok: hata bir değer, onu bir üst katmana sen taşırsın.
Go ekibinin FAQ (sık sorulan sorular) sayfasındaki gerekçesi, exception'ların sıradan hataları (açılamayan bir dosya, stokta olmayan bir kitap) olağanüstü durum gibi göstermesi ve kontrol akışını görünmez yapması. Go kodunda bir fonksiyonun nerede çıkabileceğini satırlara bakarak görürsün. Bedeli de belli: if err != nil blokları kodu uzatır. Bu konuda yıllarca yeni syntax önerileri tartışıldı; Robert Griesemer'in 3 Haziran 2025 tarihli Go blog yazısına göre Go ekibi uzlaşma çıkmadığı için error handling'e yeni syntax ekleme çalışmalarını öngörülebilir bir süre için bıraktı. Yani if err != nil kalıcı; onu iyi yazmayı öğrenmek gerekiyor.
İyi yazmak üç kurala dayanıyor. Hatayı ya ele al ya da bağlam ekleyip yukarı ver, ikisini birden yapma. Hataya, çağıranın bilmediği bilgiyi ekle: hangi sipariş, hangi kitap. Çağıranın karar vermesi gereken durumları (stok yok, kayıt yok) tanınabilir yap. Bookstore'da bu üç kuralı sırayla uygulayacağız.
Sentinel Error'lar ve errors.Is
Çağıranın tanıması gereken durumlar için package seviyesinde sabit error değerleri tanımlanır. Bunlara sentinel error denir; standart kütüphanedeki io.EOF ve sql.ErrNoRows en bilinenleri. orders package'ına ayrı bir dosya ekledim:
package orders
import "errors"
// errors.Is ile test edilebilen sentinel error'lar.
var (
ErrNotFound = errors.New("order not found")
ErrOutOfStock = errors.New("out of stock")
ErrInvalidTransition = errors.New("invalid status transition")
)
// ValidationError, bir isteğin geçersiz bir alanını bildirir.
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return e.Field + ": " + e.Message
}
catalog'a da ErrBookNotFound ve ISBN (kitaplara verilen uluslararası standart numara) ile kitap bulan bir method ekledim:
// ErrBookNotFound, istenen ISBN'e sahip kitap yoksa döner.
var ErrBookNotFound = errors.New("book not found")
// ByISBN verilen ISBN'deki kitabı döndürür.
func (Store) ByISBN(isbn string) (Book, error) {
for _, b := range books {
if b.ISBN == isbn {
return b, nil
}
}
return Book{}, fmt.Errorf("isbn %s: %w", isbn, ErrBookNotFound)
}
04'teki MarkPaid artık bool değil error döndürüyor, memstore'un ByID'si de düz metin yerine fmt.Errorf("id %s: %w", id, orders.ErrNotFound) ile sentinel'ı sarıyor:
// MarkPaid Pending bir siparişi Paid yapar.
func (o *Order) MarkPaid(now time.Time) error {
if o.Status != StatusPending {
return fmt.Errorf("pay order %s in status %v: %w", o.ID, o.Status, ErrInvalidTransition)
}
o.Status = StatusPaid
o.Touch(now)
return nil
}
Sentinel'ları C#'taki exception tipleriyle eşleştirebilirsin: ErrOutOfStock, catch (OutOfStockException) bloğunun yakaladığı şeyin karşılığı. Farkı, bir tip değil bir değer olması; bu yüzden ek bilgi taşımaz. Ek bilgiyi wrapping ile ekleriz.
Wrapping: %w ile Bağlam Eklemek
Sipariş vermenin kuralları Service.Place'te. 04'teki hâlinde çağıran fiyatı ve başlığı kendisi veriyordu; artık sadece ISBN ve adet geliyor, gerisi katalogdan okunuyor. Bunun için Service'e 04'teki gibi tüketen tarafta tanımlanmış küçük bir Catalog interface'i ekledim:
// Catalog, Service'in katalogdan beklediği şeydir.
type Catalog interface {
ByISBN(isbn string) (catalog.Book, error)
}
// Line, yeni bir siparişte istenen bir kitaptır.
type Line struct {
ISBN string
Quantity int
}
// Place isteği doğrular, stoğu kontrol eder ve yeni siparişi kaydeder.
func (s *Service) Place(ctx context.Context, customerID string, lines []Line) (*Order, error) {
if err := validate(customerID, lines); err != nil {
return nil, err
}
items := make([]Item, 0, len(lines))
for _, l := range lines {
b, err := s.books.ByISBN(l.ISBN)
if err != nil {
return nil, fmt.Errorf("place order: %w", err)
}
if b.Stock < l.Quantity {
return nil, fmt.Errorf("place order: %q has %d in stock, %d requested: %w",
b.Title, b.Stock, l.Quantity, ErrOutOfStock)
}
items = append(items, Item{ISBN: b.ISBN, Title: b.Title, UnitPrice: b.Price, Quantity: l.Quantity})
}
s.nextID++ // henüz eşzamanlı kullanım için güvenli değil; 08'de düzelecek
now := s.now()
o := NewOrder(fmt.Sprintf("ORD-%d", s.nextID), customerID, now)
for _, it := range items {
o.AddItem(it, now)
}
if err := s.repo.Save(ctx, o); err != nil {
return nil, fmt.Errorf("save order %s: %w", o.ID, err)
}
return o, nil
}
fmt.Errorf'teki %w verb'ü, verilen hatayı mesajın içine yazmanın yanında onu yeni hatanın içinde saklar. Böylece bir zincir oluşur: en dıştaki hata "place order" bağlamını, içteki "isbn ..." bağlamını, en içteki de sentinel'ı taşır. errors.Is bu zinciri baştan sona dolaşır. %v kullansaydık mesaj aynı görünürdü, ama zincir kopardı ve errors.Is sentinel'ı bulamazdı. Kural basit: çağıranın hatayı tanıması gerekiyorsa %w, sadece mesaj için %v.
Bir tasarım ayrıntısı daha: ID sayacını bütün kontroller geçtikten sonra artırıyorum. İlk denememde sayaç döngüden önceydi ve stok yüzünden reddedilen her sipariş bir numara harcıyordu; lab'daki ilk başarılı sipariş ORD-1003 olmuştu. Hata yolları da bir davranış; onları da düşünerek yazmak gerekiyor. Lab programı hepsini sırayla deniyor:
// Lab 05: sentinel error'lar, wrapping, errors.Is/As/AsType ve errors.Join.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"bookstore/internal/catalog"
"bookstore/internal/orders"
"bookstore/internal/orders/memstore"
)
func main() {
ctx := context.Background()
repo := memstore.New()
svc := orders.NewService(repo, catalog.Store{})
// 1) Yeterli stok yok: sarılmış bir sentinel error.
_, err := svc.Place(ctx, "ada", []orders.Line{{ISBN: "9780141439549", Quantity: 5}})
fmt.Println(err)
fmt.Println(errors.Is(err, orders.ErrOutOfStock), err == orders.ErrOutOfStock)
// 2) Bilinmeyen ISBN: katalogun sentinel'ı iki sarmadan geçiyor.
_, err = svc.Place(ctx, "ada", []orders.Line{{ISBN: "9780000000000", Quantity: 1}})
fmt.Println(err)
fmt.Println(errors.Is(err, catalog.ErrBookNotFound))
// 3) Aynı anda birden fazla doğrulama hatası: errors.Join.
_, err = svc.Place(ctx, "", []orders.Line{{ISBN: "9780441172719", Quantity: 0}})
fmt.Printf("%v\n---\n", err)
var ve orders.ValidationError
if errors.As(err, &ve) {
fmt.Println("errors.As:", ve.Field)
}
if v, ok := errors.AsType[orders.ValidationError](err); ok {
fmt.Println("errors.AsType:", v.Field, "-", v.Message)
}
// 4) Happy path, ardından geçersiz bir durum geçişi.
o, err := svc.Place(ctx, "ada", []orders.Line{{ISBN: "9780441172719", Quantity: 1}})
if err != nil {
fmt.Println("unexpected:", err)
return
}
err = o.MarkPaid(time.Now())
fmt.Println(o, err)
err = o.MarkPaid(time.Now())
fmt.Println(err, errors.Is(err, orders.ErrInvalidTransition))
// 5) Log'layıp return etmek: aynı hata iki kez görünür.
logger := log.New(os.Stdout, "", 0)
if err := placeAndLog(ctx, svc, logger); err != nil {
logger.Printf("handler: %v", err)
}
}
func placeAndLog(ctx context.Context, svc *orders.Service, logger *log.Logger) error {
_, err := svc.Place(ctx, "grace", []orders.Line{{ISBN: "9780547928227", Quantity: 1}})
if err != nil {
logger.Printf("place failed: %v", err) // burada log'luyor...
return err // ...çağıran da tekrar log'luyor
}
return nil
}
go run ./labs/05-errors
place order: "Middlemarch" has 4 in stock, 5 requested: out of stock
true false
place order: isbn 9780000000000: book not found
true
customerId: is required
items[0].quantity: must be positive
---
errors.As: customerId
errors.AsType: customerId - is required
order ORD-1001 (Paid): 1 items, 349.90 TRY <nil>
pay order ORD-1001 in status Paid: invalid status transition true
place failed: place order: "The Hobbit" has 0 in stock, 1 requested: out of stock
handler: place order: "The Hobbit" has 0 in stock, 1 requested: out of stock
İlk iki satır wrapping'in iki yüzünü gösteriyor. Mesaj, ne olduğunu okuyana tek satırda anlatıyor: Middlemarch'tan 4 tane var, 5 istendi. errors.Is zincirde ErrOutOfStock'u buldu, == karşılaştırması ise bulamadı, çünkü dıştaki hata artık başka bir değer. Sarılmış hatalarla == kullanma. Üçüncü ve dördüncü satırda katalogun sentinel'ı iki katmandan geçip yine tanındı. Go'da bu mesajlar küçük harfle başlar ve nokta ile bitmez, çünkü başka mesajların içine eklenecekler.
Custom Error Tipi, errors.Join ve errors.AsType
Sentinel bir değer olduğu için ek bilgi taşımaz. Hangi alanın neden geçersiz olduğunu taşımak için ValidationError gibi bir struct tanımlarsın; Error() string method'u olduğu sürece her tip bir error'dır. C#'ta Exception'dan türeyen kendi exception class'ının karşılığı, ama base class yok. Doğrulama fonksiyonu ilk hatada durmak yerine hepsini topluyor:
// validate ilk hatada durmak yerine bütün sorunları toplar.
func validate(customerID string, lines []Line) error {
var errs []error
if customerID == "" {
errs = append(errs, ValidationError{Field: "customerId", Message: "is required"})
}
if len(lines) == 0 {
errs = append(errs, ValidationError{Field: "items", Message: "must not be empty"})
}
for i, l := range lines {
if l.Quantity <= 0 {
errs = append(errs, ValidationError{Field: fmt.Sprintf("items[%d].quantity", i), Message: "must be positive"})
}
}
return errors.Join(errs...) // errs boşsa nil döner
}
errors.Join (Go 1.20) birden fazla hatayı tek bir hatada birleştirir; mesajları alt alta yazar ve hepsini zincirde tutar. Liste boşsa nil döndürür. Bu ayrıntı önemli: 04'teki nil interface tuzağını hatırla. var e *ValidationError; return e yazsaydık, hiçbir sorun yokken bile nil olmayan bir error dönerdi ve her sipariş reddedilirdi. errors.Join hiç hata yoksa gerçekten nil bir interface döndürüyor.
Zincirdeki bir tipi bulmanın iki yolu var. errors.As bir hedef değişken ister ve bulduğu ilk eşleşmeyi oraya yazar. Go 1.26'daki errors.AsType aynı işi generic bir fonksiyonla yapıyor: func AsType[E error](err error) (E, bool). Önceden bir değişken tanımlayıp adresini vermek gerekmiyor, sonuç ve bool tek satırda geliyor; 1.26 release notes'u da onu daha hızlı ve tip güvenli diye tanıtıyor. İkisi de errors.Join'in içine bakıyor ve ilk eşleşmeyi, yani customerId hatasını buluyor. Bütün alanları istiyorsan zinciri kendin dolaşırsın; bunu 10'da, doğrulama hatalarını HTTP cevabına çevirirken yapacağız.
Pointer mı Değer mi? Go 1.27'nin Yeni vet Kontrolü
Önce Tahmin Et'e dönelim. ValidationError'ın Error() method'u value receiver'lı. 04'teki method set kuralına göre value receiver'lı method'lar hem değerin hem pointer'ın method set'inde; yani hem ValidationError hem *ValidationError bir error. Fonksiyon &ValidationError{...} döndürünce zincirde pointer tipi duruyor, errors.As(err, &ve) ise değer tipini arıyor ve bulamıyor. Testte her şey doğru görünür, çünkü mesaj aynı; hata, birisi hatanın tipine göre karar vermeye çalıştığında ortaya çıkar.
Go 1.27'nin vet'indeki printf kontrolü tam olarak bu kalıbı arıyor: %w'ye verilen argüman *E tipindeyse ve E zaten error ise uyarı veriyor. Kaynak koda baktığımda kontrolün dosyanın Go sürümünün en az 1.27 olmasını şart koştuğunu gördüm; bu yüzden go 1.26.0 satırıyla sessiz kaldı. Bookstore da go 1.26.0'da olduğu için bu kontrolden yararlanmıyor. Ekipte bir kural koy: error tipinin method'u value receiver'lıysa her yerde değerini kullan, pointer receiver'lıysa her yerde pointer'ını. ValidationError küçük ve değişmeyen bir tip, bu yüzden Bookstore'da değer olarak kullanıyoruz.
Hatayı HTTP Cevabına Çevirmek
Service katmanı hatayı tanınabilir hâle getirdi; HTTP katmanı ise onu bir status code'a çevirir. API'ye GET /api/books/{isbn} endpoint'ini ekledim. {isbn} bir path wildcard'ı ve değerini r.PathValue("isbn") verir; routing'in ayrıntılarını 09'da göreceğiz. 01'de söz verdiğim düzeltmeyi de yaptım: Encode'un döndürdüğü hata artık görmezden gelinmiyor.
mux.HandleFunc("GET /api/books/{isbn}", func(w http.ResponseWriter, r *http.Request) {
book, err := store.ByISBN(r.PathValue("isbn"))
if errors.Is(err, catalog.ErrBookNotFound) {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err != nil {
log.Printf("get book: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
writeJSON(w, book)
})
// writeJSON v'yi response body olarak yazar. Body yazılmaya başladıktan sonra
// status code değiştirilemez, bu yüzden encoding hatası sadece log'lanır.
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Printf("write json: %v", err)
}
}
curl -i http://127.0.0.1:52710/api/books/9780141439587
curl -i http://127.0.0.1:52710/api/books/9780000000000
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 87
{"isbn":"9780141439587","title":"Emma","author":"Jane Austen","price":19900,"stock":7}
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Content-Length: 35
isbn 9780000000000: book not found
Bilinen bir durum (kitap yok) 404 oldu ve mesaj istemciye gitti. Bilinmeyen bir hata ise log'a yazılıyor, istemciye sadece genel bir mesaj gidiyor; iç hataların ayrıntısını dışarı vermemek güvenlik açısından da doğru. ASP.NET Core'da bu eşlemeyi bir exception middleware'inde merkezi olarak yapardık. Go'da da merkezi bir yere taşınabilir; 10'da hata cevaplarını Problem Details (RFC 9457) biçiminde döndüren ortak bir fonksiyon yazacağız.
panic ve recover: Ne Zaman ve Nerede?
Go'da exception'a en yakın şey panic. Bir panic, fonksiyonu durdurur, call stack boyunca defer'ları çalıştırarak yukarı çıkar ve kimse onu recover ile durdurmazsa programı bitirir. 03'te nil map'e yazınca bunu görmüştük. Ama panic, iş kuralı hatası için değil, programcı hatası için: nil pointer, sınır dışı index, "olamaz" dediğin bir durum. Stok yok, kayıt bulunamadı, istek geçersiz gibi durumlar error'dır. Kural kısa: bir library, çağıranı bir hata yüzünden panic'e sokmamalı.
HTTP server'da bir handler panic olursa ne olur? Bir lab ile denedim: aynı bozuk handler bir kez olduğu gibi, bir kez de internal/httpapi package'ına koyduğum Recover middleware'iyle sarılmış olarak bağlı:
// Lab 05: handler'daki bir panic'in httpapi.Recover ile ve onsuz yaptığı şey.
package main
import (
"log"
"net/http"
"bookstore/internal/httpapi"
)
func main() {
broken := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("price list not loaded") // bir programcı hatası, istek hatası değil
})
mux := http.NewServeMux()
mux.Handle("GET /plain", broken)
mux.Handle("GET /safe", httpapi.Recover(broken))
addr := "127.0.0.1:52711"
log.Printf("lab listening on http://%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
curl -sS -i http://127.0.0.1:52711/plain
curl -i http://127.0.0.1:52711/safe
curl: (52) Empty reply from server
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Content-Length: 22
internal server error
Server'ın log'u (stack trace kısaltıldı, dosya yolları yer tutucuyla değiştirildi):
2026/11/15 04:15:40 http: panic serving 127.0.0.1:54449: price list not loaded
goroutine 20 [running]:
net/http.(*conn).serve.func1()
/usr/local/go/src/net/http/server.go:1963 +0xac
panic({0x100ed31d0?, 0x100e840f8?})
/usr/local/go/src/runtime/panic.go:859 +0x120
main.main.func1({0x9f770881e0?, 0x9f770a013c?}, 0x9f770dda98?)
/home/user/bookstore/labs/05-recover/main.go:13 +0x2c
...
2026/11/15 04:15:40 panic serving GET /safe: price list not loaded
Process çökmedi: net/http her bağlantıyı kendi goroutine'inde çalıştırır ve o goroutine'de panic'i kendisi recover eder, stack trace'i log'lar ve bağlantıyı kapatır. Ama istemci hiçbir HTTP cevabı alamadı, curl "Empty reply from server" dedi. Angular istemcisi bunu bir network hatası olarak görür, 500 olarak değil. Recover middleware'i ise panic'i yakalayıp düzgün bir 500 cevabı yazdı ve tek satırlık bir log bıraktı:
// Package httpapi, Bookstore komutlarının ortak HTTP yardımcılarını tutar.
package httpapi
import (
"log"
"net/http"
)
// Recover, next'teki bir panic'i 500 cevabına ve tek bir log satırına çevirir.
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
v := recover()
if v == nil {
return
}
if v == http.ErrAbortHandler {
panic(v) // net/http bu değeri bir cevabı bilerek kesmek için kullanır
}
log.Printf("panic serving %s %s: %v", r.Method, r.URL.Path, v)
http.Error(w, "internal server error", http.StatusInternalServerError)
}()
next.ServeHTTP(w, r)
})
}
recover sadece defer edilmiş bir fonksiyonun içinde işe yarar ve sadece aynı goroutine'deki panic'i yakalar. http.ErrAbortHandler, net/http'nin bir cevabı bilerek yarıda kesmek için kullandığı özel bir panic değeri; onu tekrar fırlatıyoruz. API'de bütün mux'ı bu middleware ile sardım: http.ListenAndServe(addr, httpapi.Recover(mux)). ASP.NET Core Web API serisinin "Global Hata Yönetimi" yazısındaki exception middleware'inin Go'daki karşılığı bu. Log'a stack trace eklemeyi 13'te, log/slog ile structured log kurarken yapacağız. Middleware zincirinin kendisini 09'da konuşacağız.
Üretimde Dikkat
Her hata bir kez log'lanmalı, o da hatanın ele alındığı yerde. Lab'ın son iki satırı aynı stok hatasını iki kez yazdı: biri placeAndLog'da, biri handler'da. Gerçek bir serviste bu, alarmlarda hata sayısının iki katına çıkması ve aynı olayın log'da üç dört kez görünmesi demek. Alt katmanlar bağlam ekleyip hatayı yukarı verir; log'u en üstteki handler yazar.
Hata mesajları log'a ve bazen istemciye gider. İçine şifre, token ya da kişisel veri koyma. Bookstore'da hata mesajında sipariş ID'si ve kitap başlığı var; müşterinin adresi ya da kart bilgisi asla olmamalı.
Sentinel'ları ve error tiplerini package'ın public API'si gibi düşün. orders.ErrOutOfStock'u export ettiğin an, başka package'lar ona göre karar verir. Adını değiştirmek ya da kaldırmak bir breaking change. Çağıranın tanıması gerekmeyen hataları sentinel yapma, düz fmt.Errorf yeter.
Sık Düşülen Tuzaklar
Hatayı _ ile yutmak. result, _ := strconv.ParseInt(s, 10, 64) derlenir ve geçersiz girdide sessizce 0 kullanır. Bir hatayı bilerek yok sayıyorsan nedenini bir yorumla yaz.
Bağlamsız mesaj. Beş katmandan "not found" diye yukarı çıkan bir hata, log'da neyin bulunamadığını söylemez. Her katman kendi bilgisini ekler: "place order: isbn 9780000000000: book not found".
Sarılmış hatayı == ile karşılaştırmak. Lab'da gördüğümüz gibi err == orders.ErrOutOfStock false döner. errors.Is kullan.
Error tipinin değerini ve pointer'ını karıştırmak. Önce Tahmin Et'teki errors.As hatası bu. Bir tip için tek bir biçim seç; module'ün go satırını 1.27'ye çıkardığında vet bu karışıklığı senin için yakalar.
Akış kontrolü için panic kullanmak. C#'tan gelen biri bazen "exception fırlat, yukarıda yakala" alışkanlığını panic ve recover ile taklit eder. Go'da bu kod okunmaz hâle gelir ve defer'lar beklenmedik sırada çalışır.
Küçük Bir Deneme
★☆☆ labs/05-errors'a, olmayan bir sipariş ID'siyle repo.ByID çağıran bir adım ekle ve errors.Is(err, orders.ErrNotFound)'un true döndüğünü göster. Sonra memstore'daki %w'yi %v yap ve sonucu karşılaştır.
★★☆ Lab'daki placeAndLog'u, log'lamak yerine hataya bağlam ekleyip döndürecek şekilde düzelt: fmt.Errorf("place for %s: %w", customerID, err). Çıktıda kaç satır kaldı ve son satır neyi anlatıyor?
★★★ validate'in döndürdüğü birleşik hatadaki bütün ValidationError'ları bir slice'a toplayan ValidationErrors(err error) []ValidationError fonksiyonunu yaz. İpucu: errors.Join'in döndürdüğü hata Unwrap() []error method'unu sağlar; bir type assertion ile ona ulaşabilirsin.
Dilin temel parçalarını gördük; dil bloğunun son yazısı kaldı. Bir sonraki yazıda generics ve iterator'lara geçiyoruz: type parameter'lar ve constraint'ler, C# generics'ten farkları, iter.Seq ile range-over-func ve Go 1.27'de method'lara gelen type parameter'lar. Örnek olarak generic bir Page[T] sayfalama tipi ve kitapları yazara göre gruplayan bir iterator yazacağız.