Har bir til oxir-oqibat bir xil savolga javob berishi kerak: funksiya so'ralgan ishni bajara olmasa nima bo'ladi? Aksariyat tillar exception bilan javob beradi — bu stack'ni biror narsa ushlamaguncha yechib boradigan alohida boshqaruv oqimi yo'li. Go boshqacha javob beradi. Goda xato — oddiy qiymat, boshqa har qanday qiymat kabi qaytariladi, uni qayta ishlash esa oddiy if operatori. Bu tanlov Go dizaynidagi eng ko'p bahs qilingan qarorlardan biri, va u Go kodi qanday yozilishining deyarli barcha boshqa jihatlarini shakllantiradi.
Bu yondashuvning foydasi 2015-yilda Rob Pike'ning "Errors are values" nomli maqolasida ochiq aytilgan: xatolarga alohida mexanizm emas, oddiy ma'lumot sifatida qarash ular bilan boshqa hamma narsa uchun ishlatiladigan vositalar bilan ishlash imkonini beradi. Ularni saqlash, solishtirish, o'rash, log qilish yoki butun bir to'plamini nima qilishni hal qiladigan funksiyaga uzatish mumkin. Yashirin boshqaruv oqimi yo'q — bajarilishni uch stack frame tepada jimgina yo'naltiradigan try bloki mavjud emas. Goda funksiya muvaffaqiyatsiz bo'la olsa, bu fakt uning signaturasida ko'rinadi, va har bir chaqiruvchi bu haqda nima qilishni aniq hal qilishi kerak. Bu ko'rinuvchanlik — kelishuv: Go kodida Java yoki Pythondagi ekvivalentidan ko'ra ko'proq if err != nil satrlari bor, lekin hech kim ushlashni o'ylamagani sababli qayta ishlanmay, exception log'lari orasida yo'qolib ketadigan xatolar ancha kam.
Goda xato — oddiy qiymat, boshqa har qanday qiymat kabi qaytariladi; uni qayta ishlash oddiy
if.
error interface'iButun mexanizm standart kutubxonadagi eng kichik interface'lardan biriga tayanadi:
type error interface {
Error() string
}
Shuncha. Error() string metodiga ega har qanday tip — struct, int, satr o'rami — error interface'ini qanoatlantiradi. Bu ataylab shunday: Goda o'rnatilgan Exception bazaviy klassi yoki qat'iy xato turlari ierarxiyasi yo'q. Xato — shunchaki "o'zini satr sifatida tasvirlay oladigan narsa", qolgan hamma narsa shu bitta metod ustiga qurilgan kelishuv.
Muvaffaqiyatsiz bo'lishi mumkin bo'lgan funksiya errorni oxirgi natija sifatida qaytaradi:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide: division by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
Bu misolda ikkita kelishuv katta ish qiladi. Birinchidan, error doim oxirgi qaytariladigan qiymat: Go dasturchilari bunga shunchalik tayanadiki, uni buzadigan kod xato kabi ko'rinadi. Ikkinchidan, nil xato — muvaffaqiyat degani; buni tekshirish uchun alohida boolean yoki status kodi yo'q. Har qanday Go kod bazasida minglab marta uchraydigan if err != nil idiomasi aynan bitta narsani tekshiradi: bu amal nil bo'lmagan xato qiymatini hosil qildimi?
nil bu yerda shunchaki o'rin egallovchi emas, mazmunli ekanini yaxshi tushunib olish kerak. Har qanday interface, jumladan errorning ham nol qiymati nil. Muvaffaqiyatli bajariladigan funksiya xato qiymatini umuman yaratmaydi, shuning uchun nol qiymat o'zgarishsiz o'tib ketadi. Bu Goning boshqa hamma joyda uchraydigan nol-qiymat falsafasining o'zi (10-bobga qarang): muvaffaqiyat maxsus "OK" belgisi bilan emas, qiymatning yo'qligi bilan ifodalanadi.
Go xato yaratishning uchta usulini beradi, va vaziyatga mosini tanlash amalda "yaxshi xato qayta ishlash"ning katta qismini tashkil qiladi.
errors.New mumkin bo'lgan eng oddiy xatoni yaratadi — formatlashsiz qat'iy satr:
import "errors"
err := errors.New("connection pool exhausted")
Xabar statik bo'lsa va qo'shiladigan ma'lumot bo'lmasa, undan foydalaning.
fmt.Errorf — asosiy ishchi vosita. U fmt.Sprintf kabi ishlaydi, faqat natijasi error bo'ladi:
err := fmt.Errorf("invalid port %d: must be between 1 and 65535", port)
Qo'lda yozadigan deyarli har bir xatoingiz fmt.Errorf orqali o'tishi kerak, chunki deyarli har bir foydali xato xabari muvaffaqiyatsizlikni keltirib chiqargan aniq kirish yoki holatdan biror bo'lakni o'z ichiga oladi. Yalang'och errors.New("invalid port") kelajakdagi debugger'ga qaysi port ekani yoki qaysi chaqiruvda yuz bergani haqida hech narsa aytmaydi.
Maxsus xato tiplari xabar o'rniga (yoki u bilan birga) tuzilgan ma'lumotni olib yuradi. Bu chaqiruvchilarga satrni tahlil qilish o'rniga nima noto'g'ri ketganini dasturiy tarzda tekshirish imkonini beradi:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
Pointer receiver'ga e'tibor bering: errorni ValidationError emas, *ValidationError amalga oshiradi. Bu keng tarqalgan chalkashlik manbai, chunki errors.As (keyingi bobda yoritiladi) bu tip bilan ishlashi uchun pointer'ga pointer talab qiladi. Error()ni pointer tipida aniqlash — standart kelishuv. Bu shuningdek *ValidationErrorning nol qiymati nil ekanini anglatadi, va funksiya (*ValidationError)(nil)ni error sifatida qaytarganda "nil xato — xato yo'q" degan umumiy taxminga mos keladi.
Oxirgi jumlada tajribali Go dasturchilarini ham adashtiradigan nozik tuzoq yashiringan: agar funksiyaning e'lon qilingan qaytariladigan tipi error interface'iga o'ralgan aniq pointer tipi bo'lsa, shu aniq tipdagi nil pointer nil interface qiymati bo'lmaydi.
func doWork() *ValidationError { return nil }
func run() error {
var err *ValidationError = doWork()
return err // nil *ValidationError'ni ushlab turgan nil bo'lmagan error interface'ini qaytaradi!
}
error interface qiymati — juftlik: aniq tip va aniq qiymat. Bu yerdagi return err (*ValidationError, nil) juftligini interface'ga o'raydi, va bu juftlik interface'ning o'z nol qiymatiga, (nil, nil)ga teng emas. Hech narsa noto'g'ri ketmagan bo'lsa ham, chaqiruvchining if err != nil sharti true bo'ladi. Tuzatish deyarli har doim bir xil: funksiyalarni aniq xato tipini emas, error qaytaradigan qilib e'lon qiling va tiplangan nil o'zgaruvchi o'rniga aniq return nil yozing.
Sentinel xato — chaqiruvchilar to'g'ridan-to'g'ri solishtirishi mumkin bo'lgan paket darajasidagi error qiymati, bitta aniq, taniqli holatni bildirish uchun ishlatiladi:
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func Lookup(id string) (*Record, error) {
rec, ok := store[id]
if !ok {
return nil, ErrNotFound
}
return rec, nil
}
Standart kutubxona bu andozadan keng foydalanadi — io.EOF, sql.ErrNoRows, os.ErrNotExist — chunki u chaqiruvchilarga tekshirish uchun barqaror va arzon narsa beradi:
rec, err := Lookup(id)
if errors.Is(err, ErrNotFound) {
// aniq "topilmadi" holatini qayta ishlash
}
Sentinel xatolar holatlar kichik, qat'iy to'plam bo'lganda va ularning hech biriga qo'shimcha ma'lumot biriktirish kerak bo'lmaganda yaxshi mos keladi. Qaysi yozuv topilmagani yoki ruxsat to'qnashuvi nimadan iborat ekanini aytish kerak bo'lgan zahoti ular yaramaydi. Bunday holatda maydonlarni olib yuradigan maxsus xato tipi yaxshiroq vosita, chunki errors.As tuzilgan ma'lumotni qaytarib chiqara oladi (34-bobga qarang). Foydali qoida: sentinel xatolar "qaysi holat yuz berdi" degan savolga javob beradi, maxsus xato tiplari esa "nima yuz berdi va mana tafsilotlari" degan savolga.
Sentinel xatolar yana bir narsani keltirib chiqaradi: ular paketingizning ochiq API'sining bir qismiga aylanadi. ErrNotFoundni eksport qilish chaqiruvchilarga bu muvaffaqiyatsizlik turi barqaror va unga tayanish mumkin degan va'da beradi. Shuning uchun keyinroq nomini o'zgartirishingiz yoki olib tashlashingiz mumkin bo'lgan narsa uchun sentinel eksport qilmang, va aslida hech qanday ajratuvchi xatti-harakatga ega bo'lmagan "xato yuz berdi" holati uchun sentinel yaratmang.
Xato xabarlari shunchaki satrlar bo'lgani uchun va xato chaqiruv stack'i bo'ylab qayta-qayta o'ralganda bu satrlar ko'pincha bir-biriga ulanib ketgani uchun, Goda ularni yozishning kuchli "uy uslubi" bor. go vet uni faqat yumshoq tekshiradi, ekotizimdagi code review kelishuvi esa uni qat'iy ta'minlaydi:
"Failed to connect." emas, "failed to connect": shunda boshqa fmt.Errorf uni o'raganda bo'lak tabiiy o'qiladi: "starting server: failed to connect"."open config.yaml: permission denied" "error reading file"dan foydaliroq, chunki u aniq resursni nomlaydi."error in dialTCP function: connection refused" o'rniga "dial tcp: connection refused"ni afzal ko'ring: funksiya nomi kontekstini xabar emas, chaqiruv joyi olib yurishi kerak.Bular uslub bo'yicha mayda-chuyda emas. Ular shuning uchun mavjudki, xato tarqalganda Go xabarlari ko'pincha bir-biriga qo'shilib boradi:
// chaqiruv stack'ining chuqurligida
return fmt.Errorf("open %s: %w", path, err)
// bir daraja yuqorida
return fmt.Errorf("load config: %w", err)
// eng tepada
return fmt.Errorf("startup: %w", err)
Tepadagi chaqiruvchi startup: load config: open config.yaml: permission denied xabarini ko'radi — har bir qatlamda nima sinab ko'rilayotganining o'qiladigan izi, faqat kichik, yaxshi ifodalangan bo'laklardan qurilgan. Bu aslida Goning stack trace o'rniga taklif qiladigan yengil muqobili, va u faqat har bir qatlam kichik harf, tinish belgisisiz kelishuviga amal qilgandagina yaxshi o'qiladi.
Yuqoridagi misollarda %w spetsifikatori paydo bo'lganini payqagan bo'lsangiz kerak. Bu fmt.Errorfning xatoni o'rash format spetsifikatori (wrap format verb) bo'lib, u asl xatoni yangi xato ichida saqlab qoladi, shunda uni keyinchalik errors.Is va errors.As bilan tahlil qilish mumkin bo'ladi. Xatolarni o'rash keyingi 34-bobda batafsil ko'rib chiqiladi.
Ushbu bobdan olinadigan asosiy xulosalar:
panic chaqirishdan ko'ra error qaytarishni odat qiling._ orqali e'tiborsiz qoldirishdan ko'ra, har doim if err != nil bilan tekshiring.// XATO: xatoni tashlab yuborish jiddiy muammolarga olib keladi
result, _ := doSomething()
// TO'G'RI: xatoni tekshirish va kontekst bilan o'rab qaytarish
result, err := doSomething()
if err != nil {
return fmt.Errorf("do something: %w", err)
}
Go tilida har bir qaytarilgan xatoni tekshirish — shunchaki tavsiya emas, barqaror dasturiy ta'minotning eng muhim qoidasidir.
Java, Python yoki C++ dan kelgan dasturchilar uchun Go tilining xatolar bilan ishlash uslubi dastlab g'ayritabiiy ko'rinishi mumkin: Goda xatolar aniq (explicit) va lokal tekshiriladi, exception'lar esa yashirin va uzoqdan boshqariladi. Exception bir necha qatlam pastda tashlanib, ancha yuqorida ushlab qolinishi mumkin. Goda esa xato har bir qatlamda nazorat qilinishi, kontekst qo'shilib o'ralishi yoki ongli ravishda yuqoriga qaytarilishi kerak. Bu kodda ko'proq if err != nil yozishni talab qilsa-da, funksiyaning xatti-harakatini 100% shaffof va oldindan bashorat qilinadigan qiladi.
Albatta, Goda favqulodda vaziyatlar uchun panic va recover mexanizmi ham mavjud — bu haqda 35-bobda batafsil to'xtalamiz. Endi esa xatolarni kontekst bilan boyitish — ularni o'rash (wrapping) va tekshirish qoidalarini ko'rib chiqamiz.