Go’da xatolarni boshqarish
Dastur ishlayotganda hamma amal muvaffaqiyatli tugamaydi. Masalan, dastur ochmoqchi bo‘lgan fayl mavjud bo‘lmasligi mumkin. Foydalanuvchi noto‘g‘ri qiymat kiritishi mumkin. Tarmoq orqali yuborilgan so‘rov ham turli sabablarga ko‘ra bajarilmasligi mumkin.
Bular dasturda uchrashi tabiiy bo‘lgan holatlar. Go’da bunday kutiladigan muammolar odatda error qiymati orqali boshqariladi.
Har bir funksiya error qaytarishi shart emas. Agar funksiya bajarilishi muvaffaqiyatsiz tugashi mumkin bo‘lsa, u odatda asosiy natija bilan birga error ham qaytaradi.
Masalan, ikkita sonni qo‘shish kabi oddiy funksiya ko‘pincha xato qaytarishga muhtoj emas. Lekin fayl ochish, ma’lumotlar bazasiga ulanish yoki foydalanuvchi kiritgan qiymatni tekshirish kabi amallar muvaffaqiyatsiz tugashi mumkin. Bunday funksiyalarda error qaytarish tabiiy hisoblanadi.
error nima?
Go’da error alohida built-in interface hisoblanadi. U faqat bitta metodni talab qiladi:
Interface turning qanday ma’lumot saqlashini emas, u qaysi metodlarni bajarishi kerakligini belgilaydi. Hozircha
errorni Error() string metodiga ega qiymat deb tushunish yetarli. Keyingi interface darsida bu
tushuncha va turning interface talabiga qanday mos kelishi batafsil ko‘rib chiqiladi.
Bu yerda asosiy fikr juda sodda: Error() metodi xatoni odam o‘qishi mumkin bo‘lgan string ko‘rinishida ifodalaydi.
Agar biror turda quyidagi signature’ga ega metod mavjud bo‘lsa:
u error interface’ini bajarishi mumkin.
Funksiya muvaffaqiyatli bajarilganda error o‘rniga odatda nil qaytariladi. Xato yuz berganda esa muammoning sababini ifodalovchi haqiqiy error qiymati qaytariladi.
Quyidagi funksiya ikkita float64 sonni bo‘ladi:
package main
import (
"errors"
"fmt"
)
func bolish(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("nolga bo‘lish mumkin emas")
}
return a / b, nil
}
func main() {
natija, err := bolish(10, 4)
if err != nil {
fmt.Println("Xato:", err)
return
}
fmt.Println("Natija:", natija)
}
Natija:
bolish() ikkita qiymat qaytaryapti:
Birinchi qiymat — hisob natijasi.
Ikkinchi qiymat — xato haqida ma’lumot.
b nol bo‘lmasa, bo‘lish muvaffaqiyatli bajariladi:
Bu yerda haqiqiy hisob natijasi va nil qaytariladi. nil xato yo‘qligini bildiradi.
Agar b == 0 bo‘lsa:
funksiya hisobni davom ettirmaydi. float64 turning nol qiymati bo‘lgan 0 va xato qaytariladi.
Bu yerda muhim qoida bor: xato qaytarilgan holatda birinchi natijaning o‘zi ko‘pincha ishonchli emas. Chaqiruvchi avval errni tekshirishi kerak.
Xatoni tekshirish
error qaytaradigan funksiya chaqirilganda odatda birinchi qilinadigan ish — err qiymatini tekshirish.
Masalan:
Natija:
Bu yerda bolish(10, 0) xato qaytardi.
Shuning uchun:
sharti true bo‘ladi va xato boshqariladi.
return orqali esa qolgan kod bajarilmaydi:
Bu yaxshi amaliyot. Sababi xato yuz bergandan keyin qaytgan asosiy natija ko‘pincha ishlatishga yaroqsiz bo‘ladi.
Masalan, faylni o‘qish funksiyasi xato qaytargan bo‘lsa, undan kelgan data bilan odatdagidek ishlash noto‘g‘ri bo‘lishi mumkin.
Shuning uchun xato aniqlanganda odatda quyidagi qarorlardan biri qabul qilinadi:
- xatoni yuqoridagi funksiyaga qaytarish;
- foydalanuvchiga tushunarli xabar ko‘rsatish;
- boshqa yo‘l bilan amalni qayta bajarish;
- ma’lum xato turi uchun alohida fallback ishlatish;
- tizim chegarasida xatoni logga yozish.
Bu “har bir funksiyada albatta err tekshiriladi” degani emas. Faqat funksiya error qaytarsa, chaqiruvchi bu qiymat bilan nima qilishini ongli ravishda hal qilishi kerak.
Ayrim hollarda xatoni ataylab e’tiborsiz qoldirish ham mumkin. Lekin bunday qarorning sababi koddan aniq ko‘rinishi yaxshi.
Masalan:
Bu yerda xato ongli ravishda tashlab yuborilganini _ ko‘rsatadi. Lekin resursni yopishdagi xato muhim bo‘lsa, uni ham tekshirish kerak bo‘lishi mumkin.
Xato yaratish
Sodda va o‘zgarmas matnli xato yaratish uchun errors.New() ishlatiladi.
Masalan:
errors.New() berilgan matndan yangi error qiymati yaratadi.
Bu usul xato matni oldindan ma’lum bo‘lsa qulay.
Lekin ba’zan xato xabariga runtime’da olingan qiymatni ham qo‘shish kerak bo‘ladi. Masalan, aynan qaysi yosh noto‘g‘ri ekanini ko‘rsatmoqchimiz.
Bunda fmt.Errorf() qulay:
func yoshniTekshir(yosh int) error {
if yosh < 0 {
return fmt.Errorf("noto‘g‘ri yosh: %d", yosh)
}
return nil
}
Agar yosh == -5 bo‘lsa, xato taxminan quyidagicha bo‘ladi:
%d orqali yosh qiymati xato matniga qo‘shildi.
Xato matnini yozishda odatda bir necha kichik qoidaga amal qilinadi.
Xabar qisqa bo‘lishi kerak, lekin muammoning sababini tushuntirsin.
Odatda xato matni kichik harf bilan boshlanadi:
va oxiriga nuqta qo‘yilmaydi.
Buning sababi xato yuqori qatlamlarda boshqa matn bilan birlashtirilishi mumkin.
Masalan:
Agar ichki xato bosh harf va nuqta bilan yozilgan bo‘lsa, zanjirlangan xabarlar kamroq tabiiy ko‘rinishi mumkin.
Xatoni yuqoriga uzatish
Funksiya xatoni o‘zi hal qila olmasa, uni chaqiruvchiga qaytaradi.
Eng sodda variant:
Bu ishlaydi. Lekin muammo shundaki, yuqori qatlam faqat asl xatoni ko‘radi. Xato aynan qaysi amal vaqtida yuz bergani tushunarsiz bo‘lib qolishi mumkin.
Shuning uchun xatoga kontekst qo‘shish foydali.
Quyidagi misolda xato bir necha qatlam orqali yuqoriga uzatiladi:
package main
import (
"fmt"
"os"
)
func faylniOqish(nom string) ([]byte, error) {
data, err := os.ReadFile(nom)
if err != nil {
return nil, fmt.Errorf("%q faylini o‘qish: %w", nom, err)
}
return data, nil
}
func sozlamaniYuklash() ([]byte, error) {
data, err := faylniOqish("config.json")
if err != nil {
return nil, fmt.Errorf("sozlamani yuklash: %w", err)
}
return data, nil
}
func run() error {
data, err := sozlamaniYuklash()
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
func main() {
if err := run(); err != nil {
fmt.Println("Dastur ishga tushmadi:", err)
}
}
Bu oqimni bosqichma-bosqich ko‘ramiz.
Avval:
chaqiriladi.
U esa:
funksiyasini chaqiradi.
faylniOqish() ichida:
bajariladi.
Agar config.json mavjud bo‘lmasa, os.ReadFile() xato qaytaradi.
faylniOqish() bu xatoni shunchaki uzatmaydi. U qo‘shimcha kontekst qo‘shadi:
Natijada ichki xato endi qaysi amal vaqtida yuz bergani bilan birga qaytadi.
Keyin sozlamaniYuklash() ham o‘z kontekstini qo‘shadi:
Oxirida main() tizimning yuqori chegarasida xatoni foydalanuvchiga chiqaradi.
config.json mavjud bo‘lmasa, xabar taxminan quyidagicha bo‘ladi:
Dastur ishga tushmadi: sozlamani yuklash: "config.json" faylini o‘qish: open config.json: no such file or directory
Bu xabarni ichkaridan tashqariga o‘qish mumkin:
— operatsion tizim darajasidagi asl sabab.
— qaysi amal bajarilayotgan edi.
— bu amal kattaroq jarayonning qaysi qismi edi.
— xato foydalanuvchiga qaysi kontekstda ko‘rsatilmoqda.
Har bir qatlam faqat o‘ziga tegishli ma’lumotni qo‘shdi.
Yana bir muhim jihat: pastki funksiyalar xatoni ekranga chiqarib, keyin yana yuqoriga qaytarmayapti.
Masalan, faylniOqish() ichida bunday qilinmagan:
Agar har bir qatlam xatoni logga yozib, keyin qaytarsa, bir xil xato loglarda bir necha marta takrorlanishi mumkin.
Ko‘pincha yaxshi yondashuv — pastki qatlamlarda xatoga kontekst qo‘shish, yuqori tizim chegarasida esa uni bir marta chiqarish yoki logga yozish.
%w nima qiladi?
fmt.Errorf() ichidagi %w maxsus ma’noga ega:
U asl xatoni yangi xato ichiga o‘raydi, ya’ni wrap qiladi.
Natijada ikki narsa sodir bo‘ladi:
- xato matniga qo‘shimcha kontekst qo‘shiladi;
- asl xato zanjir ichida saqlanib qoladi.
Shuning uchun keyinroq errors.Is() yoki errors.As() orqali ichki sababni topish mumkin.
Agar %w o‘rniga %v ishlatilsa:
xato matni ko‘rinishda deyarli bir xil bo‘lishi mumkin.
Lekin asl error zanjir sifatida saqlanmaydi. Bu esa keyinchalik xatoni turiga yoki sababiga qarab tekshirish imkonini yo‘qotadi.
errors.Is() bilan sababni tekshirish
Xatoni matni orqali tekshirish yaxshi usul emas.
Masalan, bunday kod mo‘rt hisoblanadi:
Xato matni platformaga, kutubxonaga yoki qo‘shilgan kontekstga qarab o‘zgarishi mumkin.
Ma’lum bir xato qiymati zanjir ichida bor yoki yo‘qligini tekshirish uchun errors.Is() ishlatiladi.
package main
import (
"errors"
"fmt"
"os"
)
func faylniOqish(nom string) ([]byte, error) {
data, err := os.ReadFile(nom)
if err != nil {
return nil, fmt.Errorf("%q faylini o‘qish: %w", nom, err)
}
return data, nil
}
func main() {
_, err := faylniOqish("config.json")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Sozlama fayli topilmadi")
return
}
if err != nil {
fmt.Println("Xato:", err)
}
}
Bu yerda os.ReadFile()dan kelgan xato fmt.Errorf() va %w orqali o‘ralgan:
Shunga qaramay:
xatolar zanjirini ichkariga qarab tekshiradi.
Agar zanjir ichida os.ErrNotExistga mos xato topilsa, true qaytaradi.
Demak, xatoga qancha kontekst qo‘shilganidan qat’i nazar, dastur asl sababga qarab qaror qabul qila oladi.
Sentinel error
Dasturga tegishli doimiy va alohida xato holati uchun oldindan xato qiymati e’lon qilish mumkin:
Bunday qiymat ko‘pincha sentinel error deb ataladi.
Masalan, chaqiruvchi aynan foydalanuvchi topilmagan holatni boshqa xatolardan ajratishi kerak bo‘lsa:
Sentinel error har bir xato uchun kerak emas.
U odatda chaqiruvchi aynan shu holatni dasturiy ravishda aniqlab, unga alohida munosabat bildirishi kerak bo‘lganda foydali.
Agar xato faqat foydalanuvchiga matn ko‘rsatish uchun kerak bo‘lsa, alohida sentinel qiymat yaratish ortiqcha bo‘lishi mumkin.
Custom error va errors.As()
Ba’zan xatoda faqat matn yetarli bo‘lmaydi.
Masalan, forma tekshirayotganda aynan qaysi maydon noto‘g‘ri ekanini va muammo sababini alohida saqlamoqchimiz.
Bunday vaziyatda custom error type, ya’ni maxsus xato turi yaratish mumkin.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func userYarat(name string, age int) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "bo‘sh bo‘lmasligi kerak",
}
}
if age < 0 {
return &ValidationError{
Field: "age",
Message: "manfiy bo‘lmasligi kerak",
}
}
return nil
}
func register(name string, age int) error {
if err := userYarat(name, age); err != nil {
return fmt.Errorf("foydalanuvchini ro‘yxatdan o‘tkazish: %w", err)
}
return nil
}
func main() {
err := register("", 20)
if err == nil {
return
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("%s maydonida xato: %s\n", validationErr.Field, validationErr.Message)
return
}
fmt.Println("Kutilmagan xato:", err)
}
Natija:
Avval maxsus struct yaratildi:
Bu struct ikkita alohida ma’lumot saqlaydi:
Field— xato qaysi maydonda yuz bergani;Message— muammoning izohi.
Keyin pointer receiver bilan Error() metodi yozildi:
Shu sabab *ValidationError error interface’ini bajaradi.
Masalan:
oddiy error sifatida qaytarilishi mumkin.
Keyingi qatlamda xato wrap qilinadi:
Demak, main()ga kelgan err to‘g‘ridan-to‘g‘ri *ValidationError bo‘lmasligi mumkin. Tashqarida fmt.Errorf() yaratgan xato turibdi.
Shuning uchun oddiy type assertion qilish o‘rniga errors.As() ishlatiladi:
errors.As() xatolar zanjirini tekshiradi.
Agar ichkarida *ValidationError topilsa, uning qiymatini:
o‘zgaruvchisiga yozadi.
Shundan keyin custom error ichidagi strukturaviy ma’lumotlardan foydalanish mumkin:
Bu faqat xato matnini parse qilishdan ancha ishonchli.
errors.Is() va errors.As() farqi
Ularning vazifasi o‘xshash ko‘rinsa ham, maqsadi boshqa.
errors.Is():
ma’lum xato qiymati yoki sabab zanjir ichida mavjudligini tekshiradi.
errors.As():
ma’lum xato turini zanjirdan topadi va shu turdagi qiymatni olish imkonini beradi.
Qisqacha:
errors.Is()— “shu xato sabablar orasida bormi?”;errors.As()— “shu turdagi xato bormi va uning ma’lumotlarini bera olasanmi?”.
defer va resurslarni yopish
Dastur fayl, socket, database connection yoki boshqa resursni ochsa, uni kerakli vaqtda yopish ham muhim.
Go’da defer funksiyaning keyinroq bajarilishi kerak bo‘lgan chaqiruvini rejalashtirish imkonini beradi.
Deferred chaqiruv joriy funksiya tugashidan oldin bajariladi.
Fayl bilan ishlashda bu ayniqsa qulay:
package main
import (
"fmt"
"os"
)
func faylHajmi(nom string) (int64, error) {
file, err := os.Open(nom)
if err != nil {
return 0, fmt.Errorf("faylni ochish: %w", err)
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return 0, fmt.Errorf("fayl ma’lumotini olish: %w", err)
}
return info.Size(), nil
}
func main() {
size, err := faylHajmi("config.json")
if err != nil {
fmt.Println("Xato:", err)
return
}
fmt.Println("Fayl hajmi:", size)
}
Bu yerda avval fayl ochiladi:
Agar faylni ochishning o‘zi muvaffaqiyatsiz bo‘lsa:
funksiya darhol qaytadi.
Muhim joy: defer file.Close() faqat fayl muvaffaqiyatli ochilgandan keyin yozilgan:
Bu to‘g‘ri, chunki os.Open() xato qaytargan bo‘lsa, ishlatish mumkin bo‘lgan ochiq file resursi yo‘q.
Keyin:
bajariladi.
Agar shu joyda xato yuz bersa ham:
funksiya tugashidan oldin deferred file.Close() ishlaydi.
Muvaffaqiyatli holatda ham aynan shu narsa sodir bo‘ladi.
Shuning uchun defer barcha chiqish yo‘llarida resursni yopishni osonlashtiradi.
Aks holda har bir returndan oldin alohida:
yozishga to‘g‘ri kelishi mumkin edi.
Bir nechta defer
Bir funksiyada bir nechta defer bo‘lishi mumkin.
Ular LIFO tartibida bajariladi: oxirgi qo‘shilgan defer birinchi bajariladi.
Masalan:
funksiya tugaganda bajarilish tartibi:
Bu resurslarni teskari tartibda yopish uchun qulay.
Masalan, avval A resursini, keyin B resursini ochgan bo‘lsak, odatda avval B, keyin A yopilishi tabiiy.
defer va uzun sikllar
defer joriy blok tugaganda emas, funksiya tugaganda bajariladi.
Shuning uchun uzoq davom etadigan sikl ichida juda ko‘p defer yig‘ish ehtiyotkorlikni talab qiladi.
Masalan:
for _, name := range names {
file, err := os.Open(name)
if err != nil {
continue
}
defer file.Close()
}
Bu yerda har bir fayl iteratsiya tugaganda emas, butun funksiya tugaganda yopiladi.
Agar fayllar ko‘p bo‘lsa, bir vaqtning o‘zida juda ko‘p ochiq resurs qolib ketishi mumkin.
Bunday vaziyatda sikl tanasini alohida funksiyaga chiqarish yoki resursni kerakli joyda bevosita yopish ma’qul bo‘lishi mumkin.
panic va recover
Go’da panic mavjud, lekin u oddiy xatolarni qaytarishning o‘rnini bosmaydi.
Kutiladigan muammolar odatda error orqali qaytarilishi kerak.
Masalan:
- fayl topilmadi;
- foydalanuvchi noto‘g‘ri qiymat kiritdi;
- tarmoq uzildi;
- ma’lumotlar bazasida kerakli yozuv topilmadi.
Bular odatiy xato oqimiga kiradi.
panic esa dastur odatdagidek davom eta olmaydigan holatlar yoki dasturchi qilgan jiddiy mantiqiy xatolar uchun ishlatilishi mumkin.
panic yuz berganda joriy funksiyaning oddiy bajarilishi to‘xtaydi va stack bo‘ylab yuqoriga tarqalish boshlanadi. Shu jarayonda deferred funksiyalar bajariladi.
recover() esa deferred funksiya ichida panic qiymatini ushlashi mumkin.
package main
import "fmt"
func xavfsizIshgaTushir(fn func()) {
defer func() {
if value := recover(); value != nil {
fmt.Println("Panic ushlandi:", value)
}
}()
fn()
}
func main() {
xavfsizIshgaTushir(func() {
panic("kutilmagan ichki holat")
})
}
Natija:
Oqimni bosqichma-bosqich ko‘ramiz.
Avval:
chaqiriladi.
Funksiya ichida deferred anonim funksiya ro‘yxatdan o‘tkaziladi:
Keyin:
bajariladi.
Uzatilgan funksiya ichida:
chaqiriladi.
Oddiy bajarilish shu nuqtada to‘xtaydi.
xavfsizIshgaTushir() tugashidan oldin uning deferred funksiyasi ishlaydi.
recover() panic bilan yuborilgan qiymatni oladi:
va panic tarqalishini shu goroutine ichida to‘xtatadi.
Bu yerda muhim noziklik bor.
recover() bajarilishni panic(...) yozilgan qatordan keyin davom ettirmaydi.
Masalan:
recover() yuqori qatlamda panicni ushlasa ham:
ishga tushmaydi.
Panic yuz bergan funksiya yakunlangan hisoblanadi. Boshqaruv panicni ushlagan deferred funksiya tugagandan keyin yuqoridagi normal oqimga qaytishi mumkin.
recover() qayerda ishlatiladi?
recover()ni har bir funksiya ichiga qo‘yish kerak emas.
U ko‘pincha tizim chegarasida ishlatiladi.
Masalan, HTTP server bitta request handler ichidagi kutilmagan panic sabab butun server processini to‘xtatishni istamasligi mumkin.
Shunday chegarada recover() panicni ushlab, xatoni logga yozishi va shu requestni xato bilan tugatishi mumkin.
Lekin bu oddiy biznes xatolarini panic orqali uzatish kerak degani emas.
Masalan, bunday yondashuv tavsiya etilmaydi:
agar user topilmasligi odatiy va kutiladigan holat bo‘lsa.
Bunday vaziyatda oddiy error qaytarish to‘g‘riroq.
Xatolar bilan ishlash tartibi
Amaliy kodda xatolar bilan ishlash uchun quyidagi tartib foydali.
-
Xato yuz berishi mumkin bo‘lgan funksiya
errorqaytarsin. -
Chaqiruvchi xatoni shu qatlamda mazmunli hal qila olsa, shu yerning o‘zida qaror qabul qilsin.
Masalan, fayl topilmasa default konfiguratsiyani ishlatish mumkin bo‘lsa, bu aynan shu qatlamda hal qilinishi mumkin.
- Agar qatlam xatoni hal qila olmasa, foydali kontekst qo‘shib yuqoriga qaytarsin.
Masalan:
- Yuqori qatlam ma’lum xato sababiga alohida munosabat bildirishi kerak bo‘lsa,
errors.Is()ishlatsin.
Masalan:
-
Xatodan strukturaviy ma’lumot olish kerak bo‘lsa,
errors.As()ishlatsin. -
Xatoni foydalanuvchiga ko‘rsatish yoki logga yozishni odatda tizim chegarasida bir marta bajaring.
-
panicni kutiladigan xatolar uchun ishlatmang.
Bu yondashuv xato oqimini aniq saqlaydi. Pastki qatlamlar texnik sababni yo‘qotmaydi, yuqori qatlamlar esa foydalanuvchi yoki tizim uchun mos qaror qabul qila oladi.
Keyingi amaliy darslardan birida xatoni stderrga yozish va mos exit code qaytarishni buyruq qatori dasturi misolida ko‘ramiz.
Misollar
1. O‘zgarmas xato yaratish
Bu misol oldindan yaratilgan xato qiymatini qayta ishlatishni ko‘rsatadi.
Bo‘sh nom alohida xato holati hisoblanadi:
package main
import (
"errors"
"fmt"
)
var ErrEmptyName = errors.New("nom bo‘sh bo‘lmasligi kerak")
func validateName(name string) error {
if name == "" {
return ErrEmptyName
}
return nil
}
func main() {
err := validateName("")
if err != nil {
fmt.Println("Xato:", err)
}
}
Bu yerda:
package darajasida bitta xato qiymati yaratadi.
validateName() nomni tekshiradi:
Bo‘sh string qiymati:
string turning zero value qiymati hisoblanadi.
Bu misolda u “nom kiritilmagan” holatini bildiradi.
Nom mavjud bo‘lsa:
qaytariladi.
Nom bo‘sh bo‘lsa esa aynan ErrEmptyName qaytariladi.
Oldindan e’lon qilingan bunday xato qiymatining foydasi shundaki, chaqiruvchi kerak bo‘lsa uni keyinchalik:
bilan ham tekshirishi mumkin.
2. Xato xabariga qiymat qo‘shish
Bu misolda fmt.Errorf() yordamida aynan qaysi yosh qiymati xato ekanini xabarga qo‘shamiz.
package main
import "fmt"
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("yosh manfiy bo‘lmasligi kerak: %d", age)
}
return nil
}
func main() {
if err := validateAge(-4); err != nil {
fmt.Println("Xato:", err)
return
}
fmt.Println("Yosh qabul qilindi")
}
validateAge() ichidagi shart:
faqat manfiy qiymatlarni rad etadi.
Masalan:
xato hisoblanadi.
Lekin 0 avtomatik ravishda xato deb olinmayapti.
Shuning uchun:
ishlatilgan, age <= 0 emas.
Bu biznes qoidasiga bog‘liq. Agar tizimda yosh albatta 1 yoki undan katta bo‘lishi kerak bo‘lsa, shart boshqacha bo‘lishi mumkin edi.
Xato hosil qilinayotgan joy:
Bu yerda %d kelgan int qiymatini xabarga qo‘shadi.
age == -4 bo‘lsa:
hosil bo‘ladi.
Bu debugging va foydalanuvchiga muammoni tushuntirishda oddiy statik matndan ko‘ra foydaliroq bo‘lishi mumkin.
3. Natija bilan birga xato qaytarish
Bu misol funksiya bir vaqtning o‘zida asosiy natija va error qaytarishini ko‘rsatadi.
Omborda mavjud miqdordan ko‘p mahsulot so‘ralgan bo‘lsa, amal bajarilmaydi.
package main
import (
"errors"
"fmt"
)
func remaining(stock, requested int) (int, error) {
if requested < 1 {
return 0, errors.New("so‘ralgan miqdor musbat bo‘lishi kerak")
}
if requested > stock {
return 0, errors.New("omborda yetarli mahsulot yo‘q")
}
return stock - requested, nil
}
func main() {
left, err := remaining(12, 5)
if err != nil {
fmt.Println("Xato:", err)
return
}
fmt.Println("Qoldi:", left)
}
Avval so‘ralgan miqdor tekshiriladi:
requested kamida 1 bo‘lishi kerak.
Keyin omborda yetarli mahsulot bor yoki yo‘qligi tekshiriladi:
Agar requested stockdan katta bo‘lsa, hisoblashning ma’nosi yo‘q.
Ikkala xato holatida ham:
qaytariladi.
int turning zero value qiymati 0.
Lekin chaqiruvchi avval:
ni tekshiradi.
Shuning uchun xato holatidagi 0ni haqiqiy natija sifatida ishlatmaydi.
Muvaffaqiyatli holatda:
bajariladi.
Berilgan qiymatlar:
Hisob:
Shuning uchun funksiya:
qaytaradi.
Natija:
4. Sentinel xatoni o‘rab uzatish
Bu misol sentinel errorni yuqori qatlamga kontekst bilan uzatish va keyin errors.Is() orqali asl sababni aniqlashni ko‘rsatadi.
package main
import (
"errors"
"fmt"
)
var ErrProductNotFound = errors.New("mahsulot topilmadi")
func findProduct(id int) error {
if id != 10 {
return ErrProductNotFound
}
return nil
}
func loadProduct(id int) error {
if err := findProduct(id); err != nil {
return fmt.Errorf("%d identifikatorli mahsulotni yuklash: %w", id, err)
}
return nil
}
func main() {
err := loadProduct(25)
if errors.Is(err, ErrProductNotFound) {
fmt.Println("Boshqa mahsulot tanlang")
return
}
fmt.Println("Mahsulot yuklandi")
}
Sentinel error:
aniq bir biznes holatini ifodalaydi.
Misolni sodda qilish uchun faqat id == 10 mavjud mahsulot deb olingan:
loadProduct(25) chaqirilganda findProduct(25) xato qaytaradi.
Lekin loadProduct() bu xatoni to‘g‘ridan-to‘g‘ri uzatmaydi:
Natijada taxminan shunday xato hosil bo‘ladi:
Bu yerda %w juda muhim.
U ErrProductNotFoundni ichki sabab sifatida saqlab qoladi.
Shuning uchun:
true qaytaradi.
Chaqiruvchi xato matnini solishtirmaydi. U xatoning haqiqiy sababiga qarab qaror qabul qiladi:
Bu sentinel error va wrapping birga qanday ishlashini ko‘rsatadigan tipik misol.
5. Maxsus xato turidan ma’lumot olish
Bu misolda xato faqat matn emas, alohida maydonlarda strukturaviy ma’lumot saqlaydi.
package main
import (
"errors"
"fmt"
)
type FieldError struct {
Field string
Value int
}
func (e *FieldError) Error() string {
return fmt.Sprintf("%s maydonida noto‘g‘ri qiymat: %d", e.Field, e.Value)
}
func validateCount(count int) error {
if count < 1 {
return &FieldError{Field: "count", Value: count}
}
return nil
}
func main() {
err := validateCount(0)
var fieldErr *FieldError
if errors.As(err, &fieldErr) {
fmt.Println("Maydon:", fieldErr.Field)
fmt.Println("Qiymat:", fieldErr.Value)
}
}
Custom error turi:
ikkita ma’lumotni alohida saqlaydi.
Field — qaysi maydon xato ekanini bildiradi.
Value — aynan qanday noto‘g‘ri qiymat kelganini saqlaydi.
Keyin Error() metodi yozilgan:
Shu sabab *FieldError error interface’iga mos keladi.
count kamida 1 bo‘lishi kerak:
Misolda 0 yuborilgan:
Shuning uchun:
mazmunidagi xato qaytariladi.
Keyin:
o‘zgaruvchisi e’lon qilinadi.
errors.As():
xato *FieldError turiga mos kelishini tekshiradi.
Mos kelsa, topilgan qiymat fieldErrga yoziladi.
Shundan keyin xato matnini parse qilish shart emas:
orqali kerakli ma’lumot bevosita olinadi.
6. O‘ralgan xatolar zanjirini ko‘rish
Bu misol %w bilan o‘ralgan xatolar ichma-ich zanjir hosil qilishini va errors.Unwrap() yordamida uni ochish mumkinligini ko‘rsatadi.
package main
import (
"errors"
"fmt"
)
func prepareReport() error {
base := errors.New("ma’lumot mavjud emas")
loadErr := fmt.Errorf("ma’lumotni yuklash: %w", base)
return fmt.Errorf("hisobotni tayyorlash: %w", loadErr)
}
func main() {
err := prepareReport()
for err != nil {
fmt.Println(err)
err = errors.Unwrap(err)
}
}
Avval eng ichki xato yaratiladi:
Keyin u wrap qilinadi:
Endi zanjir taxminan shunday:
So‘ng yana bir qatlam qo‘shiladi:
Natijada zanjir:
main() ichida sikl:
xato mavjud ekan, ishlashda davom etadi.
Avval joriy tashqi xato chiqariladi:
Keyin:
orqali bitta ichki qatlamga o‘tiladi.
Jarayon taxminan quyidagicha:
1. hisobotni tayyorlash: ma’lumotni yuklash: ma’lumot mavjud emas
2. ma’lumotni yuklash: ma’lumot mavjud emas
3. ma’lumot mavjud emas
4. nil
Eng ichki oddiy xato wrap qilinmagan bo‘lgani uchun undan keyin:
nil qaytaradi.
Shunda sikl tugaydi.
Amaliy kodda ko‘pincha zanjirni qo‘lda Unwrap() qilish shart emas. errors.Is() va errors.As() odatda kerakli tekshiruvni o‘zi bajaradi. Lekin bu misol wrapping qanday tuzilishini tushunish uchun foydali.
7. Bir nechta xatoni birlashtirish
Ba’zan bir nechta mustaqil tekshiruvni bajarib, barcha topilgan xatolarni birgalikda qaytarish kerak bo‘ladi.
Buning uchun errors.Join() ishlatilishi mumkin.
package main
import (
"errors"
"fmt"
)
var ErrNameRequired = errors.New("nom kerak")
var ErrPriceInvalid = errors.New("narx musbat bo‘lishi kerak")
func validateProduct(name string, price int) error {
var nameErr error
var priceErr error
if name == "" {
nameErr = ErrNameRequired
}
if price < 1 {
priceErr = ErrPriceInvalid
}
return errors.Join(nameErr, priceErr)
}
func main() {
err := validateProduct("", 0)
fmt.Println(err)
fmt.Println(errors.Is(err, ErrNameRequired))
fmt.Println(errors.Is(err, ErrPriceInvalid))
}
Funksiya ikkita mustaqil tekshiruv bajaradi:
va:
Muhim jihat shundaki, birinchi xato topilganda funksiya darhol qaytmayapti.
Buning o‘rniga xatolar alohida o‘zgaruvchilarda saqlanadi:
error interface turning zero value qiymati nil.
Demak, boshida:
Agar nom bo‘sh bo‘lsa:
Agar narx noto‘g‘ri bo‘lsa:
Oxirida:
chaqiriladi.
errors.Join() nil bo‘lgan elementlarni tashlab yuboradi va qolgan xatolarni bitta error qiymatiga birlashtiradi.
Misolda ikkala tekshiruv ham xato:
Shuning uchun birlashtirilgan xatoda ikkala sabab mavjud.
Natijada:
ham true, va:
ham true bo‘ladi.
Bu errors.Join()ning muhim xususiyati: birlashtirilgan xato ichidagi sabablar errors.Is() va errors.As() uchun tekshirilishi mumkin.
8. Erta qaytishda deferni bajarish
Bu misol funksiya xato sabab erta tugasa ham defer ishlashini ko‘rsatadi.
package main
import (
"errors"
"fmt"
)
func process(value int) error {
fmt.Println("Jarayon boshlandi")
defer fmt.Println("Tozalash bajarildi")
if value < 0 {
return errors.New("qiymat manfiy bo‘lmasligi kerak")
}
fmt.Println("Qiymat:", value)
return nil
}
func main() {
if err := process(-1); err != nil {
fmt.Println("Xato:", err)
}
}
Avval:
bajariladi.
Keyin:
deferred chaqiruv sifatida ro‘yxatdan o‘tadi.
Bu chaqiruv aynan shu vaqtda bajarilmaydi.
Keyin value tekshiriladi:
Misolda:
Shuning uchun funksiya:
orqali erta tugaydi.
Lekin return bajarilishidan oldin deferred chaqiruv ishlaydi:
Shundan keyingina boshqaruv main()ga qaytadi.
Natija taxminan:
Demak, defer faqat muvaffaqiyatli return nil holatida emas, xato sabab erta return bo‘lganda ham ishlaydi.
Aynan shu sabab resurslarni tozalash uchun defer juda qulay.
9. Bir nechta deferning tartibi
Bu misol deferred chaqiruvlar qaysi tartibda bajarilishini ko‘rsatadi.
package main
import "fmt"
func steps() {
defer fmt.Println("1-qadam yopildi")
defer fmt.Println("2-qadam yopildi")
defer fmt.Println("3-qadam yopildi")
fmt.Println("Asosiy ish bajarildi")
}
func main() {
steps()
}
Funksiya ishlayotganda deferlar quyidagi tartibda ro‘yxatdan o‘tadi:
Lekin ular shu tartibda bajarilmaydi.
deferlar stack kabi LIFO tartibida ishlaydi:
Ya’ni oxirgi qo‘shilgan chaqiruv birinchi bajariladi.
Avval oddiy kod bajariladi:
Keyin deferred chaqiruvlar:
tartibida ishlaydi.
To‘liq natija:
Bu tartib resurslar bilan ishlashda foydali.
Masalan, resurslar quyidagi tartibda ochilgan bo‘lsa:
ularni ko‘pincha teskari tartibda yopish kerak:
deferning LIFO xususiyati aynan shunga mos keladi.
10. panicni xatoga aylantirish
Bu misolda tizim chegarasidagi funksiya kutilmagan panic qiymatini oddiy errorga aylantiradi.
package main
import "fmt"
func safeRun(action func()) (err error) {
defer func() {
if value := recover(); value != nil {
err = fmt.Errorf("amal to‘xtadi: %v", value)
}
}()
action()
return nil
}
func main() {
err := safeRun(func() {
panic("ichki holat buzildi")
})
if err != nil {
fmt.Println("Xato:", err)
}
}
Bu misolda funksiya signature’iga e’tibor bering:
err — nomlangan qaytish qiymati.
Bu shuni anglatadiki, deferred funksiya err o‘zgaruvchisiga yozishi mumkin.
Avval defer ro‘yxatdan o‘tadi:
defer func() {
if value := recover(); value != nil {
err = fmt.Errorf("amal to‘xtadi: %v", value)
}
}()
Keyin:
chaqiriladi.
Uzatilgan funksiya:
bajaradi.
Shu zahoti action()ning odatiy bajarilishi to‘xtaydi.
Stack ochila boshlaydi va safeRun()ning deferred funksiyasi ishlaydi.
recover() panic qiymatini oladi:
Keyin bu qiymat oddiy errorga aylantiriladi:
Natijada safeRun()dan taxminan quyidagi xato qaytadi:
main() esa uni odatiy error sifatida boshqaradi:
Natija:
Bu yondashuvni oddiy tekshiruvlar uchun ishlatish kerak emas.
Masalan, foydalanuvchi noto‘g‘ri qiymat yuborsa:
qilish o‘rniga funksiya to‘g‘ridan-to‘g‘ri error qaytarishi kerak.
panicni errorga aylantirish ko‘proq server, worker yoki boshqa tizim chegaralarida kutilmagan ichki nosozlik butun jarayonni yiqitib yubormasligi uchun ishlatiladi.