Tarkibga o'tish

Go’da xatolarni boshqarish

Dastur ishlayotganda fayl topilmasligi, noto‘g‘ri qiymat kiritilishi yoki tarmoq so‘rovi bajarilmasligi mumkin. Go’da bunday kutiladigan muammolar error qiymati orqali boshqariladi.

Har bir funksiya error qaytarmaydi. Faqat bajarilishi muvaffaqiyatsiz tugashi mumkin bo‘lgan funksiya odatda natija bilan birga error qaytaradi.

error nima?

error standart interfeys bo‘lib, bitta metodga ega:

type error interface {
    Error() string
}

Xato bo‘lmasa, funksiya nil qaytaradi. Xato yuz bersa, uning sababini ifodalovchi error qiymati qaytariladi.

Quyidagi funksiya ikki 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:

Natija: 2.5

bolish() muvaffaqiyatli bajarilganda hisob natijasi va nil qaytaradi. Ikkinchi argument nol bo‘lsa, natijaning nol qiymati va xato qaytariladi.

Xatoni tekshirish

Xato qaytaradigan funksiya chaqirilganda avval err tekshiriladi:

natija, err := bolish(10, 0)
if err != nil {
    fmt.Println("Xato:", err)
    return
}

fmt.Println(natija)

Natija:

Xato: nolga bo‘lish mumkin emas

Xatodan keyin natijani ishlatish xavfli bo‘lishi mumkin. Shu sababli xato aniqlanganda uni qaytarish, foydalanuvchiga tushunarli xabar ko‘rsatish yoki boshqa mos qaror qabul qilish kerak.

Bu “har bir funksiyada xato tekshiriladi” degani emas. Faqat funksiya error qaytarsa, chaqiruvchi uni e’tiborsiz qoldirmasligi kerak. Ayrim hollarda xatoni ataylab e’tiborsiz qoldirish mumkin, lekin sabab koddan aniq ko‘rinishi lozim.

Xato yaratish

O‘zgarmas matnli sodda xato uchun errors.New() ishlatiladi:

if yosh < 0 {
    return errors.New("yosh manfiy bo‘lmaydi")
}

Xato xabariga qiymat qo‘shish kerak bo‘lsa, fmt.Errorf() qulay:

func yoshniTekshir(yosh int) error {
    if yosh < 0 {
        return fmt.Errorf("noto‘g‘ri yosh: %d", yosh)
    }
    return nil
}

Xato matni qisqa va muammoning sababini tushuntiradigan bo‘lsin. Odatda xato matni kichik harf bilan boshlanadi va oxiriga nuqta qo‘yilmaydi. Bu xatoga yuqori qatlamlarda qo‘shimcha matn biriktirishni osonlashtiradi.

Xatoni yuqoriga uzatish

Funksiya xatoni o‘zi hal qila olmasa, uni chaqiruvchiga qaytaradi. Faqat return err qilish mumkin, ammo xato qaysi amal paytida yuz bergani yo‘qolib qoladi.

Quyidagi misolda xato uch 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)
    }
}

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

Har bir qatlam o‘zi bajarayotgan amal haqida kontekst qo‘shdi. Xato faqat foydalanuvchiga xabar berish mumkin bo‘lgan yuqori qatlamda chiqarildi. Pastki funksiyalar xatoni chiqarib, yana qaytarmadi. Bu bitta xatoning logda bir necha marta takrorlanishiga yo‘l qo‘ymaydi.

%w asl xatoni o‘raydi (wrap). Natijada xato matniga kontekst qo‘shiladi va asl sababni keyin ham tekshirish mumkin bo‘ladi. Oddiy %v matnni qo‘shadi, lekin xatolar zanjirini saqlamaydi.

errors.Is() bilan sababni tekshirish

O‘ralgan xatoni matni orqali solishtirish ishonchsiz. Ma’lum xato qiymatini zanjir ichidan topish 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)
    }
}

faylniOqish() xatoga kontekst qo‘shgan bo‘lsa ham, errors.Is() zanjir ichidagi os.ErrNotExist sababini topadi.

Dasturga tegishli doimiy xato holati uchun sentinel error e’lon qilish mumkin:

var ErrUserNotFound = errors.New("foydalanuvchi topilmadi")

Chaqiruvchi uni errors.Is(err, ErrUserNotFound) bilan tekshiradi. Sentinel error faqat chaqiruvchi shu holatga alohida munosabat bildirishi kerak bo‘lganda foydali.

Custom error va errors.As()

Xatoga faqat matn emas, qo‘shimcha ma’lumot ham biriktirish kerak bo‘lsa, maxsus xato turi (custom error type) yaratiladi.

Masalan, forma maydonidagi xatoda maydon nomini alohida saqlaymiz:

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:

name maydonida xato: bo‘sh bo‘lmasligi kerak

ValidationError Error() metodiga ega bo‘lgani uchun error interfeysini bajaradi. register() uni %w bilan o‘ragan. errors.As() zanjir ichidan *ValidationError turini topib, uning Field va Message maydonlarini ishlatish imkonini berdi.

Qisqacha farq:

  • errors.Is() ma’lum xato qiymati yoki sabab mavjudligini tekshiradi;
  • errors.As() ma’lum xato turini topadi va uning ma’lumotlarini olishga yordam beradi.

defer va resurslarni yopish

defer funksiya tugashidan oldin bajariladigan chaqiruvni rejalashtiradi. U fayl kabi ochilgan resursni barcha chiqish yo‘llarida yopish uchun 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)
}

defer file.Close() faqat fayl muvaffaqiyatli ochilgandan keyin yozildi. file.Stat() xato qaytarsa ham, funksiya tugashida fayl yopiladi.

Bir nechta defer oxirgi qo‘shilganidan boshlab teskari tartibda bajariladi. Ularni uzoq sikl ichida ko‘p marta yig‘ish resurslarning funksiya tugaguncha ochiq qolishiga olib kelishi mumkin.

panic va recover

panic oddiy xatoni qaytarish usuli emas. U dastur odatiy davom eta olmaydigan holat yoki dasturchi xatosi uchun ishlatiladi. Fayl topilmasligi, noto‘g‘ri foydalanuvchi kiritishi va tarmoq uzilishi kabi kutiladigan holatlar error bilan qaytariladi.

recover() deferred funksiya ichida shu goroutine’dagi panic tarqalishini to‘xtata oladi:

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:

Panic ushlandi: kutilmagan ichki holat

recover() bajarilishni panic qatoridan davom ettirmaydi. Panic yuz bergan funksiya yakunlanadi, boshqaruv esa panicni ushlagan deferred funksiya tugagach yuqori qatlamga qaytadi.

Kutubxona yoki odatiy biznes funksiyasida panicni error o‘rniga ishlatmang. recover() ham xatolarni yashirish uchun emas, masalan serverdagi bitta noto‘g‘ri so‘rov butun jarayonni to‘xtatmasligi uchun eng yuqori chegarada qo‘llanadi.

Xatolar bilan ishlash tartibi

Amalda quyidagi tartib foydali:

  1. Xato yuz berishi mumkin bo‘lgan funksiya error qaytarsin.
  2. Xatoni shu qatlam hal qila olsa, mos qaror qabul qilsin.
  3. Hal qila olmasa, %w bilan foydali kontekst qo‘shib yuqoriga qaytarsin.
  4. Chaqiruvchi alohida holatni ajratishi kerak bo‘lsa, errors.Is() yoki errors.As() ishlatsin.
  5. Xatoni foydalanuvchiga ko‘rsatish yoki logga yozishni tizim chegarasida bir marta bajaring.
  6. panicni kutiladigan xatolar uchun ishlatmang.

Xulosa

  • Faqat muvaffaqiyatsiz tugashi mumkin bo‘lgan funksiyalar odatda error qaytaradi.
  • err != nil bo‘lsa, xatoni qaytarish yoki shu joyda mazmunli boshqarish kerak.
  • fmt.Errorf() va %w xatoga kontekst qo‘shib, asl sababni saqlaydi.
  • errors.Is() xato sababini, errors.As() esa maxsus xato turini tekshiradi.
  • defer ochilgan resurslarni ishonchli yopishga yordam beradi.
  • panic/recover oddiy error oqimining o‘rnini bosmaydi.

Keyingi darsda xatoni stderr’ga yozish va mos exit code qaytarishni buyruq qatori dasturi misolida ko‘ramiz.

Misollar

1. O‘zgarmas xato yaratish

Bu misolda bo‘sh nom uchun oldindan yaratilgan xato qiymati qaytariladi.

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)
    }
}

ErrEmptyName bir marta yaratiladi va aniq xato holatini bildiradi. validateName() muvaffaqiyatli bajarilganda nil qaytaradi. Bo‘sh string turning nol qiymati bo‘lgani uchun misolda u kiritilmagan nomni ifodalaydi.

2. Xato xabariga qiymat qo‘shish

Bu misolda fmt.Errorf() noto‘g‘ri yosh qiymatini xato xabarida saqlaydi.

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")
}

age < 0 sharti faqat manfiy qiymatlarni rad etadi. 0 ham haqiqiy yosh bo‘lishi mumkin, shu sabab <= 0 ishlatilmagan. %d xabarga aynan qaysi qiymat kelganini qo‘shadi.

3. Natija bilan birga xato qaytarish

Bu misolda ombordagi mavjud miqdordan ko‘p mahsulot so‘ralsa, natijaning nol qiymati va xato qaytariladi.

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)
}

Xato holatida int turning nol qiymati 0 qaytariladi, lekin chaqiruvchi avval errni tekshiradi. Muvaffaqiyatli holatda 12 - 5 hisoblanib, 7 va nil qaytadi.

4. Sentinel xatoni o‘rab uzatish

Bu misolda pastki qatlamdagi xatoga kontekst qo‘shiladi va errors.Is() bilan asl sabab topiladi.

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")
}

%w ErrProductNotFound qiymatini xatolar zanjirida saqlaydi. Shu sabab xato matniga qo‘shimcha ma’lumot qo‘shilgan bo‘lsa ham, errors.Is() aniq sababni topadi. 10 mavjud mahsulot identifikatori sifatida tanlangan.

5. Maxsus xato turidan ma’lumot olish

Bu misolda xato turi noto‘g‘ri maydon nomi va qiymatini alohida 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)
    }
}

FieldErrorning Error() metodi bor, shuning uchun *FieldError error interfeysini bajaradi. errors.As() xatoni kerakli turga ajratib, Field va Value maydonlarini o‘qishga imkon beradi. count kamida 1 bo‘lishi kerakligi uchun 0 xato qiymat sifatida berilgan.

6. O‘ralgan xatolar zanjirini ko‘rish

Bu misolda errors.Unwrap() xatolar zanjirini tashqi qatlamdan asl sababgacha ochadi.

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)
    }
}

Har bir %w yangi tashqi qatlam yaratadi. Sikl joriy xatoni chiqarib, keyin errors.Unwrap() bilan ichki sababga o‘tadi. Eng ichki xatodan keyin Unwrap() nil qaytaradi va sikl tugaydi.

7. Bir nechta xatoni birlashtirish

Bu misolda mustaqil tekshiruvlardan chiqqan xatolar errors.Join() bilan bitta error qiymatiga birlashtiriladi.

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))
}

nameErr va priceErr dastlab nil, chunki hali xato aniqlanmagan. errors.Join() nil qiymatlarni tashlab, mavjud xatolarni birlashtiradi. Natijada errors.Is() ikkala sababni ham topadi.

8. Erta qaytishda deferni bajarish

Bu misolda funksiya xato bilan erta tugasa ham, defer orqali rejalashtirilgan tozalash bajariladi.

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)
    }
}

value manfiy bo‘lgani uchun funksiya natijani chiqarishdan oldin qaytadi. Shunga qaramay, qaytishdan avval deferred fmt.Println() bajariladi. defer xato tekshiruvidan oldin yozilgani uchun ikkala chiqish yo‘lida ham ishlaydi.

9. Bir nechta deferning tartibi

Bu misol deferred chaqiruvlar oxirgi qo‘shilganidan boshlab 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()
}

Avval asosiy ish haqidagi xabar chiqadi. Keyin deferred chaqiruvlar 3, 2, 1 tartibida bajariladi. Bu tartib ketma-ket ochilgan resurslarni teskari tartibda yopishga mos keladi.

10. panicni xatoga aylantirish

Bu misolda chegaradagi 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)
    }
}

err nomlangan qaytish qiymati sifatida e’lon qilingan. Deferred funksiya recover() orqali panic qiymatini oladi va errga yozadi. Bu usul faqat tizim chegarasidagi kutilmagan holat uchun mos; odatiy tekshiruvlarda funksiya bevosita error qaytarishi kerak.