35 / 73

panic, recover va muvaffaqiyatsizlik rejimlari

Oxirgi ikki bobdagi hamma narsa muvaffaqiyatsizlikni chaqiruvchi oqilona qayta ishlashni xohlashi mumkin bo'lgan narsa deb faraz qildi: mavjud bo'lmagan fayl, timeout bo'lgan so'rov, tekshiruvdan o'tmagan forma maydoni. Go boshqa turdagi muvaffaqiyatsizlik uchun alohida mexanizmni — panicni — saqlab qo'ygan: bu joriy goroutine'ni bajarishda davom ettirish mantiqan to'g'ri kelmaydigan holat, chunki kod tayanadigan biror invariant buzilgan. Bu chiziq qayerdan o'tishini tushunish panic va recover mexanikasini bilishdan muhimroq. Yangi boshlovchilar bu juftlik bilan qiladigan eng keng tarqalgan xato — ularni Goning try/catchi sifatida ishlatish, holbuki ular bunday emas va hech qachon bunday bo'lishi ko'zda tutilmagan.

panic aslida nima qiladi

panic chaqirish joriy funksiyaning oddiy bajarilishini darhol to'xtatadi. O'sha funksiyadagi deferred funksiyalar hamon ishlaydi — ro'yxatdan o'tgan tartibning teskarisida, oddiy return'dagi kabi. Keyin boshqaruv funksiyaning chaqiruvchisiga o'tadi, u ham to'xtaydi va o'zining deferred funksiyalarini ishga tushiradi, va hokazo chaqiruv stack'i bo'ylab yuqoriga. Agar hech narsa aralashmasa, bu yechish goroutine'ning tepasiga yetadi va dastur crash qiladi, panic qiymati va stack trace stderr'ga chop etiladi.

Til o'zi ham siz hech qayerda panic yozmagan bir necha vaziyatda panic qiladi: slice'ni chegaradan tashqari indekslash, nil pointer'ni dereference qilish, ikki qiymatli shaklsiz muvaffaqiyatsiz type assertion, butun sonni nolga bo'lish, yopilgan channel'ga yuborish. Bular aynan "dasturlash xatosi" toifasi: kodda xato bor va hech qancha if err != nil tekshiruvi to'g'ri tuzatish bo'lmasdi, chunki chaqiruvchi invariant buzilganini u buzilmaguncha bila olmasdi.

example.go
var m map[string]int
m["key"] = 1 // panic: nil map'dagi elementga qiymat berish

Bu chaqiruvchi har bir map yozuvidan oldin himoya sifatida tekshirishi kerak bo'lgan holat emas, bu map'ni initsializatsiya qilishni unutgan koddagi tuzatilishi kerak bo'lgan xato. Bundan chiqadigan sezgi: panic buzilgan invariantni bildiradi, kutilgan muvaffaqiyatsizlikni emas. Dastur usiz ishlay olmaydigan, lekin topilmagan konfiguratsiya fayli, aslida chegaradan chiqishi mumkin bo'lmagan ma'lumotdan hisoblangan slice indeksi, yopiq holatlar to'plamini sanab chiqadigan switch'ning unreachable tarmog'i — bular panic mos keladigan vaziyatlar. Ularning hammasida muvaffaqiyatsizlikning halol tavsifi "bu sodir bo'lishi mumkin emas edi", "bu ba'zan sodir bo'ladi va mana uni qanday qayta ishlash" emas.

Panic buzilgan invariantni bildiradi, kutilgan muvaffaqiyatsizlikni emas.

Buni tashqi ko'rinishi o'xshash, lekin aslida boshqacha bo'lgan holat bilan solishtiring:

example.go
// noto'g'ri: bu kutilgan, tiklanadigan holat panic sifatida kiyintirilgan
func divide(a, b float64) float64 {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

// to'g'ri: foydalanuvchi kiritishidagi nolga bo'lish buzilgan invariant emas
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide: division by zero")
    }
    return a / b, nil
}

Agar b chaqiruvchi oqilona berishi mumkin bo'lgan oddiy kirish tufayli nol bo'la olsa, bu xatolar-qiymat-sifatida bobining hududi, bu bobniki emas. Amalda yaxshi ishlaydigan sinov: agar chaqiruvchiga buni qo'zg'atishdan qanday qochish mumkinligini tushuntiruvchi jumla ("chaqirishdan oldin maxraj nol emasligini tekshiring") yoza olsangiz, bu xato bo'lishi kerak. Agar yoza oladigan yagona jumla "bu dasturda xato borligini bildiradi" bo'lsa, u panic bo'lishi mumkin.

"Panic'dan ko'ra xatolarni afzal ko'r" qoidasining tor, ataylab qilingan istisnosi bor: nomi Must bilan boshlanadigan funksiyalar kelishuvga ko'ra xato qaytarish o'rniga panic qiladi va faqat chaqiruvchi statik kafolatlay oladigan yaroqli qiymatlar bilan chaqirilishi ko'zda tutilgan — regexp.MustCompile, template.Must. Ular paket darajasidagi o'zgaruvchilarni initsializatsiya qilish kabi vaziyatlar uchun mavjud, u yerda xatoni tarqatishning oqilona usuli yo'q, yomon pattern esa baland ovozda va darhol, ideal holda kod yo'li birinchi marta bajarilganda emas, dastur ishga tushgan zahoti muvaffaqiyatsiz bo'lishi kerak.

example.go
var configPathPattern = regexp.MustCompile(`^[a-zA-Z0-9_/.-]+$`)

Agar bu pattern kompilyatsiya bo'lmasa, bu manba koddagi imlo xatosi, ish vaqtidagi biror kirish sabab bo'la olmaydigan narsa. Paket init vaqtida panic qilish yashirin xatoni uch haftadan keyingi sirli xatoga emas, darhol, baland ovozli ishga tushirish muvaffaqiyatsizligiga aylantiradi.

recover va uning chegaralari

recover davom etayotgan panic'ni to'xtatadi va panicga uzatilgan qiymatni qaytaradi. U faqat deferred funksiya ichida to'g'ridan-to'g'ri chaqirilgandagina ta'sirga ega; boshqa joyda chaqirilsa, nil qaytaradigan no-op bo'ladi.

example.go
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("something went wrong")
    // unreachable
}

defer/recover kombinatsiyasi panic'ni ushlashning yagona usuli, va uning joylashuvi ixtiyoriy emas: recover() deferred funksiyaning o'zidan emas, u chaqirgan boshqa funksiyadan chaqirilsa, ishlamaydi. Bu odamlarni chalkashtiradi, chunki u xatolarni qayta ishlash kabi birikishi kerakdek ko'rinadi, lekin birikmaydi:

example.go
func recoverHelper() {
    recover() // foydali narsa qilmaydi — deferred funksiya tomonidan to'g'ridan-to'g'ri chaqirilmagan
}

func broken() {
    defer recoverHelper()
    panic("boom") // hamon dasturni crash qiladi
}

Shu cheklov sababli recover deyarli har doim qayta ishlatiladigan yordamchi funksiya emas, kichik, literal defer func() { if r := recover(); ... }() bloki sifatida uchraydi. Bu blok odatda uch joydan birida yashaydi: goroutine'ning kirish nuqtasida (chunki har qanday goroutine'dagi, jumladan main bo'lmagan goroutine'dagi tiklanmagan panic butun dasturni crash qiladi — goroutine bo'yicha izolyatsiya yo'q), HTTP handler yoki RPC dispatch siklida (bitta yomon so'rov butun serverni yiqitmasligi kerak bo'lgan joyda) yoki mainning eng tepasida oxirgi himoya chizig'i sifatida.

example.go
// goroutine'lar: bu yerdagi tiklanmagan panic faqat shu goroutine'ni emas, butun jarayonni o'ldiradi
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("worker panic: %v", r)
        }
    }()
    process(job)
}()
example.go
// HTTP serverlar: net/http'ning o'z serveri har bir so'rov uchun panic'larni allaqachon o'zi tiklaydi,
// lekin shunga o'xshash andoza maxsus middleware yoki HTTP bo'lmagan RPC freymvorklarida keng tarqalgan
func withRecovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic handling %s: %v", r.URL.Path, rec)
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Bu oxirgi misolga alohida e'tibor berish kerak: Goning o'z net/http serveri so'rovga xizmat ko'rsatish paytida yuz bergan panic'larni allaqachon tiklaydi va log qiladi, shuning uchun handler'ning panic qilishi o'z-o'zidan jarayonni yiqitmaydi. Yuqoridagi parchadagidek maxsus recovery middleware, agar boshqa javob tanasi kerak bo'lsa, har bir panic uchun oshiriladigan metrik hisoblagich xohlasangiz yoki bu himoyani bepul olmaydigan HTTP bo'lmagan server (gRPC interceptor, message queue consumer) yozayotgan bo'lsangiz, hamon o'zini oqlaydi.

Panic'ni jimgina yutib yubormang (Silent recover qilmang)

Panic'ni recover qilib, hech narsa bo'lmagandek jim ketish — eng xavfli xatolardan biridir. Bu yaqqol ko'rinib turgan xatolikni yashirib, dasturning buzilgan va bashorat qilib bo'lmaydigan holatda ishlashini davom ettirishiga sabab bo'ladi:

example.go
// XATO: panic'ni hech qanday izsiz yutib yuborish
defer func() {
    recover()
}()

Har doim kamida tiklangan qiymatni va stack trace'ni log qiling (runtime/debug paketidagi debug.Stack() buning uchun standart vositadir, chunki panic vaqtidagi asl chaqiruvlar zanjiri yechilib bo'lgan bo'ladi).

Agar xavfsiz wrapper kerak bo'lsa, recover yordamida panic'ni oddiy error ga aylantirib qaytarish idiomatik hisoblanadi:

example.go
func runSafely(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic yuz berdi: %v", r)
        }
    }()
    fn()
    return nil
}

Crash-only dasturiy ta'minot falsafasi

Bunga bog'liq keng tarqalgan zamonaviy arxitektura tamoyili mavjud — crash-only software. Har bir noaniq nosozlikni dastur ichida qutqarishga urinib murakkab kod yozish o'rniga, komponentlarni istalgan paytda to'xtash (crash) va qayta ishga tushishga tayyor qilib loyihalash tavsiya etiladi. Qayta ishga tushirish vazifasi esa tashqi nazoratchiga (systemd, Docker, Kubernetes kabi orkestratorlarga) yuklanadi:

example.go
func main() {
    cfg := mustLoadConfig()      // konfiguratsiya xato bo'lsa, darhol panic bilan to'xtaydi
    db := mustConnectDB(cfg.DSN) // ma'lumotlar bazasi ulanmasa, xizmat ishga tushmaydi

    srv := newServer(cfg, db)
    log.Fatal(srv.ListenAndServe())
}

Bu barcha bardoshli holat tashqarida (ma'lumotlar bazasida, navbatda, obyekt xotirasida) yashaydigan, xotiradagi goroutine holatini yo'qotish hech narsaga tushmaydigan holatsiz xizmatlar uchun yaxshi ishlaydi, aksariyat tarmoq xizmatlari shunday. U xotirada qayta tiklash qimmat yoki mumkin bo'lmagan holatni saqlaydigan har qanday narsa uchun yomon ishlaydi — olti soatlik hisob-kitobning uchinchi soatiga yetgan uzoq ishlaydigan batch job "shunchaki yiqilsin va noldan qayta boshlansin" uchun yaxshi nomzod emas. Muayyan kod bo'lagi qaysi toifaga tegishli ekanini bilish — panicga qanchalik tez murojaat qilishni, qaytariladigan qiymatlar orqali xato o'tkazishga nisbatan hal qilishning bir qismi. Bu bir daraja yuqoridagi shu bobdagi masalaga tegishlimi yoki oldingisigami degan xuddi shu mulohaza qaroridir.

Ushbu bob va oxirgi ikki bobdagi umumiy g'oya shuki, Go sizga aniq ikkita vosita beradi — xato qaytar yoki panic qil — va berilgan muvaffaqiyatsizlik qaysi birini talab qilishini yozishdan oldin o'ylab ko'rgan bo'lishingizni kutadi. Aynan shu mulohazani to'g'ri qilish oldindan aytib bo'ladigan tarzda muvaffaqiyatsiz bo'ladigan Go kodini muvaffaqiyatsizlik usullari haqida jim turadigan Go kodidan ajratadi. Test yozishni boshlaganingizda bu mulohaza doimiy ishga tushadi, keyingi bob esa aynan shundan boshlanadi: kodingiz qaytaradigan xatolar va himoya qilish uchun panic qiladigan invariantlar aslida siz o'ylagandek ishlashini tekshirish.