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.