34 / 73

Xatolarni o'rash va tekshirish

Oldingi bob xatolarni shaffof bo'lmagan qiymat sifatida ko'rib chiqdi: siz uni yaratasiz, qaytarasiz, chaqiruvchi esa nilmi-yo'qmi tekshiradi. Bu model dastur bir qatlamdan katta bo'lmaguncha ishlaydi, deyarli har bir haqiqiy dastur esa shunday. Ma'lumotlar bazasi chaqiruvi repository funksiyasi ichida muvaffaqiyatsiz bo'ladi, uni service funksiyasi chaqiradi, uni HTTP handler chaqiradi. Muvaffaqiyatsizlik handler'ga yetib kelganda ikkita narsa bir vaqtda to'g'ri bo'lishi kerak: asl, aniq sabab hamon tekshirilishi mumkin bo'lsin (bu sql.ErrNoRowsmi, timeout'mi yoki boshqami?), va operatorga ko'rsatiladigan xabar drayver nima xabar berganini emas, handler nima qilayotganini tushuntirsin. Xatolarni o'rash (error wrapping) — Goning shu ikki ehtiyojga bergan javobi.

fmt.Errorf va %w format spetsifikatori

Go 1.13 versiyasida %w format spetsifikatori (wrap verb) qo'shildi. U matn chiqarishda %v kabi ishlaydi, ammo asl xatoni yangi hosil bo'lgan xato ichida saqlab qoladi:

example.go
func loadConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("load config: %w", err)
    }
    // ...
    return nil
}

Bu yerda qaytarilgan xato konsolda load config: open config.yaml: no such file or directory deb chiqadi, ammo u shunchaki oddiy satr emas. Ichki tomondan %w orqali quyidagi interfeysni implementatsiya qiluvchi maxsus o'ram yaratiladi:

example.go
type Wrapper interface {
    Unwrap() error
}

Unwrap() error metodiga ega bo'lgan har qanday xato boshqa xatoni o'rab turgan hisoblanadi. Standart kutubxonadagi errors.Is va errors.As funksiyalari ushbu zanjirni avtomatik kezib chiqishni biladi.

%w faqat fmt.Errorf bilan ishlaydi va o'zi error bo'lgan qiymatni o'raydi. Go 1.20 dan boshlab bitta satrda bir nechta %w ishlatish ham mumkin (fmt.Errorf("%w: %w", err1, err2)).

errors.Is: "Bu o'sha xatomi?" deb so'rash

O'rash paydo bo'lishidan oldin xatoni sentinel bilan solishtirish == degani edi, va == xato o'ralgan zahoti ishlamay qoladi:

example.go
var ErrNotFound = errors.New("not found")

func find(id int) error {
    return fmt.Errorf("lookup %d: %w", id, ErrNotFound)
}

err := find(1)
if err == ErrNotFound {   // false — err *fmt.wrapError, ErrNotFound o'zi emas
    // hech qachon ishlamaydi
}

errors.Is buni zanjirni qayta-qayta yechib, har bir qatlamni moslikka tekshirish orqali tuzatadi:

example.go
if errors.Is(err, ErrNotFound) {
    // true — errors.Is ErrNotFound'ni topmaguncha zanjir bo'ylab yuradi
}

errors.Is sukut bo'yicha har qadamda == ishlatadi, lekin tip Is(error) bool metodini amalga oshirib, maxsus ekvivalentlikni belgilashi mumkin. Bu "bir xil turdagi xato" "bir xil qiymat" degani bo'lmaganda foydali, masalan, turli ma'lumot olib yursa ham bir xil muvaffaqiyatsizlik toifasini ifodalaydigan xatolarni solishtirishda. os.ErrNotExist aynan shunday ehtiyojga mos keladi: turli platformalardagi fayl tizimi xatolari bir xil asosiy qiymatga ega emas.

Yaxshilab o'zlashtirish foydali bo'lgan amaliy andoza: chaqiruvchilaringiz aslida tarmoqlanishi kerak bo'lgan muvaffaqiyatsizlik rejimlari uchun kichik sentinel xatolar lug'atini tuzing va ularni tekshiriladigan hamma joyda, hatto o'z paketingiz testlarida ham errors.Is bilan tekshiring. Ba'zan err.Error() ni satr sifatida solishtirishga vasvasa bo'ladi — qarshi turing. Satr bo'yicha solishtirish aynan keyingi safar kimdir xabar matniga tekkanda jimgina buziladigan narsa.

errors.As: "Bu qanday turdagi xato?" deb so'rash

errors.Is ha/yo'q savoliga javob bersa, errors.As zanjirdan aniq xato tipini ajratib oladi, shunda u olib yurgan ma'lumotni o'qish mumkin:

example.go
type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %d not found", e.Resource, e.ID)
}

func handle(err error) {
    var nfErr *NotFoundError
    if errors.As(err, &nfErr) {
        log.Printf("missing resource: %s (id=%d)", nfErr.Resource, nfErr.ID)
        return
    }
    log.Printf("unexpected error: %v", err)
}

Bu yerda ko'rinishidan muhimroq ikkita tafsilot bor. Birinchidan, errors.Asga beriladigan nishon mos keladigan tipga pointer bo'lishi kerak — &nfErr, bunda nfErr o'zi *NotFoundError — chunki errors.As mos kelgan qiymatni unga qaytarib yozadi. Buni noto'g'ri qilsangiz (&nfErr o'rniga nfErr uzatsangiz), kompilyator buni ushlamaydi, chunki errors.Asning ikkinchi parametri any; u nil bo'lmagan pointer bo'lmagan narsa olsa, faqat ish vaqtida panic qiladi. Ikkinchidan, errors.As faqat eng tashqi xatoni emas, butun zanjirni kezib chiqadi, shuning uchun uch qatlam chuqurlikda o'ralgan *NotFoundErrorni ham yuzadagidek ishonchli topadi.

Aynan shu mexanizm maxsus xato tiplarini aniqlash foydali bo'lgan qiladi. Oddiy satr xatosini faqat inson o'qiy oladi; errors.As bilan tiklangan tipni esa kod o'qiy oladi. Bu chaqiruvchiga bitta err qiymatidan timeout bo'lsa qayta urinishni, topilmagan xatoga 404, qolgan hamma narsaga 500 qaytarishni imkon beradi.

Go 1.26 errors.As talab qiladigan "o'zgaruvchi e'lon qil, uning manzilini uzat" marosimini chetlab o'tadigan generic muqobilni qo'shdi: errors.AsType[T](err) to'g'ridan-to'g'ri (T, bool) qaytaradi.

example.go
if nfErr, ok := errors.AsType[*NotFoundError](err); ok {
    log.Printf("missing resource: %s (id=%d)", nfErr.Resource, nfErr.ID)
    return
}

Zanjir xuddi shunday kezib chiqiladi, natija ham xuddi shunday, lekin oraliq var nfErr *NotFoundError e'lon qilish va noto'g'ri berib qo'yish mumkin bo'lgan pointer-to-pointer yo'q. Type parameter nishon tipning o'zi bo'lgani uchun kompilyator siz haqiqiy xato tipini so'rayotganingizni chaqiruv joyidayoq tekshiradi, errors.Asda esa nil bo'lmagan pointer bo'lmagan narsa berilsa, xato faqat ish vaqtida chiqadi. errors.As eskirgan emas va hech qayerga ketmaydi: ko'plab mavjud kod va minimal Go versiyasini 1.26 dan yuqoriga ko'tarmagan har qanday kod bazasi undan foydalanishda davom etadi. Lekin yetarlicha yangi toolchain'dagi yangi kod uchun errors.AsType odatda aynan shu ish uchun to'g'riroq vosita.

errors.Join va ko'p xatoli daraxtlar

Go 1.20 errors.Joinni qo'shdi. U til avval toza javobga ega bo'lmagan muammoni hal qiladi: bir nechta mustaqil narsa noto'g'ri ketganda va faqat birinchisini emas, hammasini xabar qilmoqchi bo'lsangiz nima qilasiz? Uchta noto'g'ri maydonli formani tekshirish klassik misol — birinchi xatoda to'xtab, foydalanuvchini uch marta tuzatib, qayta yuborishga majburlash yomon tajriba.

example.go
var ErrNameRequired = errors.New("name is required")

func validate(name, email string, age int) error {
    var errs []error
    if name == "" {
        errs = append(errs, ErrNameRequired)
    }
    if !strings.Contains(email, "@") {
        errs = append(errs, fmt.Errorf("invalid email %q", email))
    }
    if age < 0 || age > 150 {
        errs = append(errs, fmt.Errorf("invalid age %d", age))
    }
    return errors.Join(errs...)
}

errors.Join(errs...) agar errs bo'sh bo'lsa (yoki faqat nillardan iborat bo'lsa) nil qaytaradi, aks holda Error() metodi har bir xabarni alohida qatorga joylashtiradigan bitta error qaytaradi. Muhimi, bu shunchaki satrlarni qo'shish emas: qaytarilgan qiymat o'rash interface'ining ikkinchi, ko'plik shakli — Unwrap() []error — ni amalga oshiradi, uni errors.Is ham, errors.As ham tushunadi. Ya'ni uchta xatoni birlashtirib, birlashgan qiymatni chaqiruvchiga bersangiz, chaqiruvchi hamon oddiy errors.Is chaqiruvi bilan "bulardan biri ErrNotFound edimi?" deb so'ray oladi, xuddi hamma vaqt faqat bitta xato bo'lgandek:

example.go
err := validate("", "not-an-email", 200)
fmt.Println(err)
// name is required
// invalid email "not-an-email"
// invalid age 200

if errors.Is(err, ErrNameRequired) {
    // err uchta xatodan iborat birlashgan daraxt bo'lsa ham hamon ishlaydi
}

errors.Join paydo bo'lishidan oldin odamlar Errors []error maydoni va xabarlarni qo'shadigan qo'lda yozilgan Error() metodi bilan o'zlarining MultiError tipini yozardi. Bu andozani Go 1.20 dan oldingi eski kod bazalarida va ba'zi kutubxonalarda hamon ko'rasiz. Lekin yangi kodda standart kutubxona versiyasidan foydalanish yaxshiroq, chunki u errors.Is/errors.As mosligini Is/As metodlarini o'zingiz yozmasdan, tayyor holda beradi.

%w va errors.Join birgalikda Godagi xato qiymati aslida ro'yxat emas, daraxt ekanini anglatadi: fmt.Errorf chiziqli zanjirlar hosil qiladi, errors.Join esa shoxlanuvchi tugunlar, va ikkalasi bir-birining ichiga erkin joylashadi. errors.Is va errors.As ikkalasi ham bu daraxtni chuqurlik bo'yicha (depth-first) to'liq kezib chiqadi, shuning uchun o'rash bilan birlashtirish orasidagi farq natijani yuqorida qanday tekshirishingizni o'zgartirmaydi.

Xato zanjirini qo'lda o'qish

Ba'zan — odatda debug qilishda yoki diagnostika vositasini yozishda — bitta is/as savolini berish o'rniga zanjirni o'zingiz yurib chiqish foydali bo'ladi:

example.go
func chain(err error) []error {
    var out []error
    for err != nil {
        out = append(out, err)
        err = errors.Unwrap(err)
    }
    return out
}

Shuni bilingki, bu oddiy sikl faqat yagona Unwrap() error shaklini kuzatadi va errors.Join daraxtining Unwrap() []error shoxlariga kirmaydi. Buning uchun kichik rekursiv yurish kerak bo'lardi, lekin avval errors.Is/errors.Asdan foydalaning: o'zingizning aylanib chiqishingizni yozish kamdan-kam zarur, shoxlanish holatida esa nozik xato qilish oson.

Kattalashganda ham ishlaydigan amaliyotlar

Bir nechta odat kod bazasi o'sgan sayin foydali bo'lib qoladigan xato qayta ishlashni shovqinga aylanadigan xato qayta ishlashdan ajratadi:

Chaqiruvchida boshqa yo'l bilan bo'lmaydigan ma'lumot qo'shayotganingizda o'rang — qaysi fayl, qaysi ID, konveyerning qaysi bosqichi. Sababsiz shunchaki "error" so'zini qo'shayotgan bo'lsangiz, o'rashni o'tkazib yuboring (fmt.Errorf("error: %w", err) err aytmagan hech narsani hech kimga aytmaydi). Sentinel xatolar va maxsus xato tiplarini ularni ishlab chiqaradigan paketda saqlang, har xil narsa solinadigan umumiy errors paketida emas: shunda errors.Is/errors.As tekshiruvlari ularni aslida qo'zg'ata oladigan koddan uzoqlashib ketmaydi. Va bir xil asosiy sababni ikki qo'shni qatlamda deyarli bir xil matn bilan ikki marta o'rashdan saqlaning: "failed: failed: failed: connection refused" kabi zanjir mexanik o'rashning hammasiga ega, lekin o'rashni arziydigan qiladigan qo'shilgan kontekstning hech biriga ega emas.

Tekshirish yoritilgach, keyingi savol — umuman bu tarzda qayta ishlanmasligi kerak bo'lgan muvaffaqiyatsizliklar haqida nima qilish: dasturni ishlashda davom ettirish uni to'xtatishdan yomonroq bo'ladigan vaziyatlar. Bu panic va recover hududi, keyingi bobda yoritiladi.