خطا در زبان Go: مدیریت و توسعه بهتر با error wrapping و custom errors

زمانی که در زبان Go یک تابع دچار خطا می‌شود، آن تابع یک مقدار از نوع interface خطا (error) برمی‌گرداند تا فراخواننده بتواند آن خطا را مدیریت کند. اغلب توسعه‌دهندگان از تابع fmt.Errorf در پکیج fmt برای ایجاد این مقادیر خطا استفاده می‌کنند. اما پیش از نسخه ۱.۱۳ Go، مشکلی در این روش وجود داشت و آن از بین رفتن اطلاعات مربوط به خطاهای داخلی بود. برای حل این مشکل، یا باید از پکیج‌های خارجی کمک می‌گرفتند یا خودشان struct های خطای مخصوص درست می‌کردند. در Go 1.13 ویژگی‌هایی اضافه شد که کار با خطاها را بسیار ساده‌تر و منسجم‌تر می‌کند.

ویژگی error wrapping در Go 1.13

یکی از قابلیت‌های اضافه شده، امکان wrap کردن خطا با استفاده از fmt.Errorf و استفاده از verb جدید %w است. این به شما اجازه می‌دهد خطاها را به صورت سلسله‌مراتبی بسته‌بندی کنید و خطاهای اصلی را بدون از دست دادن، نگه دارید. همچنین تابع‌های errors.Is و errors.As در پکیج errors اضافه شدند که بررسی وجود خطاهای خاص در بین مجموعه‌ای از خطاهای wrap شده را بسیار ساده می‌کنند و دسترسی مستقیم به آن‌ها را هم فراهم می‌کنند.

ایجاد برنامه با استفاده از fmt.Errorf و بررسی خطا

فرض کنیم می‌خواهیم برنامه‌ای بنویسیم که اعداد ۱ تا ۳ را اعتبارسنجی کند. اگر عدد نامعتبر باشد، یک خطا با پیام مشخص برگشت می‌دهد. تابع validateValue این کار را انجام می‌دهد. خطاها با fmt.Errorf ساخته می‌شوند و در main خروجی بر اساس خطا یا موفقیت چاپ می‌شود:

func validateValue(num int) error {
    if num == 1 {
        return fmt.Errorf("that's odd")
    }
    if num == 2 {
        return fmt.Errorf("uh oh")
    }
    return nil
}

func main() {
    for i := 1; i <= 3; i++ {
        err := validateValue(i)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println("valid!")
        }
    }
}

خروجی برنامه نشان می‌دهد که عدد ۱ با خطای “that’s odd” و عدد ۲ با خطای “uh oh” روبرو هست و عدد ۳ معتبر است.

استفاده از Sentinel Errors برای مدیریت بهتر خطاها

تست خطاها با مقایسه مستقیم رشته پیام چندان توصیه نمی‌شود، زیرا تغییر متن‌ها باعث نیاز به تغییر چندباره کد می‌شود. راه بهتر تعریف متغیرهای خطا (معروف به sentinel errors) است:

var ErrUhOh = errors.New("uh oh")

func validateValue(num int) error {
    if num == 1 {
        return fmt.Errorf("that's odd")
    }
    if num == 2 {
        return ErrUhOh
    }
    return nil
}

اکنون در main می‌توانیم خطاها را اینطوری تشخیص بدهیم:

if err == ErrUhOh {
    fmt.Println("oh no!")
} else if err != nil {
    fmt.Println("there was an error:", err)
} else {
    fmt.Println("valid!")
}

این به ساختار کد نظم می‌دهد و به راحتی قابل نگهداری‌تر است.

خطایابی پیشرفته با استفاده از error wrapping و Unwrap

وقتی خطاها پیچیده‌تر می‌شوند، wrapping خطا به کمک می‌آید تا بدون از دست رفتن خطای اصلی، اطلاعات بیشتری همراه شود:

func runValidation(num int) error {
    if err := validateValue(num); err != nil {
        return fmt.Errorf("run error: %w", err)
    }
    return nil
}
func main() {
    for i := 1; i <= 3; i++ {
        err := runValidation(i)
        if errors.Is(err, ErrUhOh) {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

تابع fmt.Errorf با %w خطا را wrap می‌کند. با errors.Is می‌توان مشخص کرد که یک خطای مشخص در هر عمق از زنجیره خطاهای wrap شده وجود دارد یا خیر، بدون نیاز به دستی unwrap کردن.

ساختار خطای سفارشی (custom error) ValueError

گاهی برای خطاها نیاز است اطلاعات بیشتری مانند مقدار ورودی نامعتبر داشته باشیم. می‌توانیم یک struct تعریف کنیم و تابع Error() را پیاده‌سازی کنیم تا این ساختار به عنوان خطا محسوب شود. همچنین برای پشتیبانی از wrapping، متد Unwrap() هم تعریف می‌کنیم:

type ValueError struct {
    Value int
    Err   error
}

func (v *ValueError) Error() string {
    return fmt.Sprintf("value error: %v", v.Err)
}

func (v *ValueError) Unwrap() error {
    return v.Err
}

func newValueError(value int, err error) error {
    return &ValueError{
        Value: value,
        Err:   err,
    }
}

func validateValue(num int) error {
    if num == 1 {
        return newValueError(num, fmt.Errorf("that's odd"))
    }
    if num == 2 {
        return newValueError(num, ErrUhOh)
    }
    return nil
}

حال خطاها با جزئیات بیشتر نمایش داده می‌شوند و امکان بررسی مقدار نامعتبر داخل خطا وجود دارد.

بررسی خطاهای wrapped با errors.Is و errors.As

تا به اینجا برای باز کردن خطاهای nested مجبور بودیم چند بار errors.Unwrap استفاده کنیم. اما errors.Is و errors.As این کار را بسیار آسان می‌کنند:

for i := 1; i <= 3; i++ {
    err := runValidation(i)
    if errors.Is(err, ErrUhOh) {
        fmt.Println("oh no!")
    } else if valueErr := new(ValueError); errors.As(err, &valueErr) {
        fmt.Printf("value error: %d had %v\n", valueErr.Value, valueErr.Err)
    } else if err != nil {
        fmt.Println("there was an error:", err)
    } else {
        fmt.Println("valid!")
    }
}

تابع errors.Is بررسی می‌کند آیا خطای مشخص شده در هر عمق از خطا wrapped شده وجود دارد یا خیر. تابع errors.As نیز تلاش می‌کند تا اگر در میان خطاهای wrap شده، خطایی از نوع مورد نظر وجود داشت، آن را در متغیر اعلام شده قرار دهد تا بتوان از فیلدهای خاص آن استفاده کرد.

نتیجه

در این آموزش، با نحوه مدیریت خطاها در زبان Go آشنا شدیم و دیدیم چگونه می‌توان خطاها را wrap کرد تا اطلاعات بیشتری ارائه دهند. همچنین متوجه شدیم چگونه از errors.Is و errors.As برای مدیریت و دسترسی به خطاهای پیچیده استفاده کنیم. استفاده از این روش‌ها علاوه بر ساده‌تر کردن کد، آینده‌نگرانه و مقاوم در برابر تغییرات ساختاری است.

 

از همراهی شما با پارمین کلود متشکریم.

Click to rate this post!
[Total: 0 Average: 0]

نظرات کاربران

دیدگاهی بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *