2.6 مدیریت خطاها

2.6 مدیریت خطاها

در این بخش قصد داریم به مقوله مدیریت خطاها در زبان گو بپردازیم و اینکه چطور می‌توانید خیلی ساده خطاها را مدیریت کنید. مدیریت خطا در زبان گو با سایر زبان‌ها متفاوت هست و شما با چیزی به نام try-catch یا try-except سروکار ندارید.

مدیریت خطاها در زبان گو به دو روش صورت می گیرد:

  • با استفاده از پیاده سازی اینترفیس error که یک روش مرسوم جهت مدیریت و نمایش خطا است.
  • با استفاده از panic/recover که در فصل اول توضیح دادیم.

2.6.1 مدیریت خطا با اینترفیس error #

روش زبان گو برای مقابله با خطا این است که به صراحت، شما خطا را به عنوان خروجی تابع برگردانید. برای این کار کافیست اگر میخواهید خطای هر تابع را مدیریت کنید، اینترفیس error را در خروجی تابع بگذارید.

https://pkg.go.dev/builtin#error

type error interface {
    Error() string
}

به مثال زیر توجه کنید:

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("non-existing.txt")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(file.Name() + "opened succesfully")
	}
}

در کد بالا ما با استفاده از تابع Open که در پکیج os وجود دارد فایل non-existing.txt را باز کرده‌ایم. اگر دقت کنید این تابع ۲ تا خروجی دارد یکی ساختار File هست و دیگری خطا هست. در ادامه ما با استفاده شرط آمدیم چک کردیم اینترفیس err آیا خالی است یا خیر؟ در کد بالا این اینترفیس خالی nil نیست و ما خطا را چاپ کردیم.

این روش به طور گسترده در پکیج‌های داخلی و شخص ثالث گو استفاده می‌شود.

دقت کنید اینترفیس error یک متد دارد به نام ()Error که این متد متن خطا را بصورت رشته بر می‌گرداند.

آیا همیشه نیاز است خطاها را مدیریت کنیم؟

شاید بپرسید آیا واقعا نیاز هست ما همیشه خطاها را مدیریت کنیم؟ در جواب این سوال می توانیم بگیم هم بله و هم خیر

  • علت اینکه می‌گوییم بله از این بابت هست اگر خطاها بدرستی مدیریت نشود احتمال اینکه با panic در هر جا مواجه شویم خیلی زیاد است. بخصوص خطای nil pointer . پس بهتر است تا جایی که می‌توانید خطاها را بدرستی مدیریت کنید و همچنین اگر جایی احتمال می‌دهید panic پیش میاد بهتر است از recover استفاده کنید تا پایداری برنامه را بالا ببرید.
  • علت اینکه می‌گوییم خیر از این بابت هست که در زبان گو، هیچ اجباری برای مدیریت خطاها وجود ندارد و گاهی اوقات می‌توانید خطاها را نادیده بگیرید که با استفاده از ـ امکان پذیر است.

2.6.2 مزایای استفاده از error به عنوان یک تایپ در زبان گو #

  • به شما این امکان را می‌دهد کنترل بیشتری رو خطاها داشته باشید و تو هر قدم می‌توانید خطاها را بررسی کنید.
  • جلوگیری از try-catch جهت مدیریت خطا (دقت کنید در سایر زبان ها باید تا جایی که ممکن است از try-catch کمتر استفاده کنید)

2.6.3 روش‌های مختلف برای ایجاد یک خطا #

در زبان گو شما می‌توانید در هرجای کد خود یک خطا با محتوای مناسب ایجاد کنید و یا اینکه برخی از خطاهای برخی از کتابخانه‌ها را هم‌پوشانی کنید.

1. با استفاده (“متن خطا”)errors.New

package main

import (
    "errors"
    "fmt"
)

func main() {
    sampleErr := errors.New("error occured")
    fmt.Println(sampleErr)
    }

در بالا ما با استفاده از تابع New پکیج errors یک خطا با متن مشخص ایجاد کردیم و متغیر sampleErr از نوع اینترفیس error می‌باشد که می‌توانید در هر جای کد خود مدیریتش کنید.

2. با استفاده از (“error is %s”, “some error message”)fmt.Errorf

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

package main

import (
    "fmt"
)

func main() {
	msg := "database connection issue"
    sampleErr := fmt.Errorf("Err is: %s", msg)
    fmt.Println(sampleErr)
}

2.6.4 ایجاد خطا پیشرفته #

در مثال زیر ما قصد داریم یک خطای پیشرفته ایجاد کنیم و آن را به آسانی مدیریت کنیم.

ویژگی‌های خطای پیشرفته :

  • در زیر inputError یک نوع ساختار است که داخلش ۲ تا فیلد message و missingField دارد و همچنین دارای یک متد ()Error است.
  • شما می‌توانید به این ساختار خطای پیشرفته، متدهای بیشتری اضافه کنید و همچنین گسترش دهید که به عنوان مثال ما متد getMissingFields را برای گرفتن محتوای missingField اضافه کردیم.
  • ما با استفاده از type assertion می‌توانیم اینترفیس error را به inputError تبدیل کنیم.
package main

import "fmt"

type inputError struct {
    message      string
    missingField string
}

func (i *inputError) Error() string {
    return i.message
}

func (i *inputError) getMissingField() string {
    return i.missingField
}

func main() {
    err := validate("", "")
    if err != nil {
        if err, ok := err.(*inputError); ok {
            fmt.Println(err)
            fmt.Printf("Missing Field is %s\n", err.getMissingField())
        }
    }
}

func validate(name, gender string) error {
    if name == "" {
        return &inputError{message: "Name is mandatory", missingField: "name"}
    }
    if gender == "" {
        return &inputError{message: "Gender is mandatory", missingField: "gender"}
    }
    return nil
}

2.6.5 نادیده گرفتن خطاها #

شما در هرجای کد خود با استفاده از _ می توانید متغیر خطا را نادیده بگیرید و آن را مدیریت نکنید. هر چند در بالا گفتیم نادیده گرفتن خطاها عوارضی در بر دارد و ما همیشه، تاکید می‌کنیم تا جایی که ممکن است خطاها را مدیریت کنید.

package main
import (
    "fmt"
    "os"
)
func main() {
    file, _ := os.Open("non-existing.txt")
    fmt.Println(file)
}

در بالا ما خطای تابع Open را نادیده گرفتیم و مقدار file را چاپ کردیم مقدار چاپ شده nil است چون تایپ خروجی با اشاره‌گر است و قطعا مقدار خالی بودش nil است.

2.6.6 هم‌پوشانی (Wrapping) خطا #

در زبان گو، شما می‌توانید خطا را با خطا و پیغام مشخصی هم پوشانی کنید. حالا هم‌پوشانی خطا چیست؟

بزارید با یک مثال ساده توضیح دهیم، فرض کنید شما تو لایه دیتابیس خود یکسری خطاها از سمت دیتابیس دریافت می‌کنید به عنوان مثال اگر شما سندی را در دیتابیس monogdb پیدا نکنید با خطای no documents found مواجه خواهید شد. شما در اینجا نمی‌توانید همان متن خطا را به کاربر نمایش دهید بلکه باید آن خطا را با یک متن خطای مناسب هم پوشانی کنید.

package main

import (
	"fmt"
)

type notPositive struct {
	num int
}

func (e notPositive) Error() string {
	return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}

type notEven struct {
	num int
}

func (e notEven) Error() string {
	return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}

func checkPositive(num int) error {
	if num < 0 {
		return notPositive{num: num}
	}
	return nil
}

func checkEven(num int) error {
	if num%2 != 0 {
		return notEven{num: num}
	}
	return nil
}

func checkPostiveAndEven(num int) error {
	if num > 100 {
		return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
	}

	err := checkPositive(num)
	if err != nil {
		return err
	}

	err = checkEven(num)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	num := 3
	err := checkPostiveAndEven(num)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Givennnumber is positive and even")
	}

}

2.6.7 Unwrap خطاها #

در بخش بالا شما با نحوه هم‌پوشانی کردن آشنا شدید، اما این امکان را داریم خطاها را unwrap کنیم با استفاده از یک تابع در پکیج errors به نام Unwrap.

func Unwrap(err error) error

منظورمان از unwrap کردن این است که، اگر خطایی را هم پوشانی کرده باشیم با استفاده unwrap می‌توانیم آن خطا را ببینیم.

import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happened"
}
func main() {
    e1 := errorOne{}
    e2 := fmt.Errorf("E2: %w", e1)
    e3 := fmt.Errorf("E3: %w", e2)
    fmt.Println(errors.Unwrap(e3))
    fmt.Println(errors.Unwrap(e2))
    fmt.Println(errors.Unwrap(e1))
}

در کد بالا متغیر e2 خطای داخل ساختار e1 را هم‌پوشانی کرده و سپس متغیر e3 خطای متغیر e2 را هم‌پوشانی می‌کند. در نهایت با تابع Unwrap متن خطای اصلی را چاپ کردیم.

2.6.8 بررسی دو خطا اگر برابر هستند #

در زبان گو شما می‌توانید ۲ اینترفیس را با هم مقایسه کنید و این مقایسه به وسیله اپراتور == یا با استفاده از تابع Is در پکیج errors صورت می‌گیرد. اساساً دو مقوله برای این مقایسه در نظر گرفته خواهد شد:

func Is(err, target error) bool
  • هر دو این اینترفیس‌ها به یک نوع تایپ منصوب شده باشند.
  • مقدار داخلی اینترفیس‌ها باید با هم برابر باشند یا اینکه هر دو (nil) باشند.
package main
import (
    "errors"
    "fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
    return "Error One happended"
}
func main() {
    var err1 errorOne
    err2 := do()
    if err1 == err2 {
        fmt.Println("Equality Operator: Both errors are equal")
    }
    if errors.Is(err1, err2) {
        fmt.Println("Is function: Both errors are equal")
    }
}
func do() error {
    return errorOne{}
}