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

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

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

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

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

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

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

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

1type error interface {
2    Error() string
3}

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

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6)
 7
 8func main() {
 9	file, err := os.Open("non-existing.txt")
10	if err != nil {
11		fmt.Println(err)
12	} else {
13		fmt.Println(file.Name() + "opened succesfully")
14	}
15}
1$ go run main.go
2open non-existing.txt: no such file or directory

در کد بالا ما با استفاده از تابع 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

 1package main
 2
 3import (
 4    "errors"
 5    "fmt"
 6)
 7
 8func main() {
 9    sampleErr := errors.New("error occured")
10    fmt.Println(sampleErr)
11    }
1$ go run main.go
2error occured

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

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

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

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7func main() {
 8	msg := "database connection issue"
 9    sampleErr := fmt.Errorf("Err is: %s", msg)
10    fmt.Println(sampleErr)
11}
1$ go run main.go
2Err is: database connection issue

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

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

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

  • در زیر inputError یک نوع ساختار است که داخلش ۲ تا فیلد message و missingField دارد و همچنین دارای یک متد ()Error است.
  • شما می‌توانید به این ساختار خطای پیشرفته، متدهای بیشتری اضافه کنید و همچنین گسترش دهید که به عنوان مثال ما متد getMissingFields را برای گرفتن محتوای missingField اضافه کردیم.
  • ما با استفاده از type assertion می‌توانیم اینترفیس error را به inputError تبدیل کنیم.
 1package main
 2
 3import "fmt"
 4
 5type inputError struct {
 6    message      string
 7    missingField string
 8}
 9
10func (i *inputError) Error() string {
11    return i.message
12}
13
14func (i *inputError) getMissingField() string {
15    return i.missingField
16}
17
18func main() {
19    err := validate("", "")
20    if err != nil {
21        if err, ok := err.(*inputError); ok {
22            fmt.Println(err)
23            fmt.Printf("Missing Field is %s\n", err.getMissingField())
24        }
25    }
26}
27
28func validate(name, gender string) error {
29    if name == "" {
30        return &inputError{message: "Name is mandatory", missingField: "name"}
31    }
32    if gender == "" {
33        return &inputError{message: "Gender is mandatory", missingField: "gender"}
34    }
35    return nil
36}
1$ go run main.go
2Name is mandatory
3Missing Field is name

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

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

1package main
2import (
3    "fmt"
4    "os"
5)
6func main() {
7    file, _ := os.Open("non-existing.txt")
8    fmt.Println(file)
9}
1$ go run main.go
2{nil}

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

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

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

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

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type notPositive struct {
 8	num int
 9}
10
11func (e notPositive) Error() string {
12	return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
13}
14
15type notEven struct {
16	num int
17}
18
19func (e notEven) Error() string {
20	return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
21}
22
23func checkPositive(num int) error {
24	if num < 0 {
25		return notPositive{num: num}
26	}
27	return nil
28}
29
30func checkEven(num int) error {
31	if num%2 == 1 {
32		return notEven{num: num}
33	}
34	return nil
35}
36
37func checkPostiveAndEven(num int) error {
38	if num > 100 {
39		return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
40	}
41
42	err := checkPositive(num)
43	if err != nil {
44		return err
45	}
46
47	err = checkEven(num)
48	if err != nil {
49		return err
50	}
51
52	return nil
53}
54
55func main() {
56	num := 3
57	err := checkPostiveAndEven(num)
58	if err != nil {
59		fmt.Println(err)
60	} else {
61		fmt.Println("Givennnumber is positive and even")
62	}
63
64}
1$ go run main.go
2checkEven: Given number 3 is not an even number

2.6.7 Unwrap خطاها #

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

1func Unwrap(err error) error

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

 1import (
 2    "errors"
 3    "fmt"
 4)
 5type errorOne struct{}
 6func (e errorOne) Error() string {
 7    return "Error One happened"
 8}
 9func main() {
10    e1 := errorOne{}
11    e2 := fmt.Errorf("E2: %w", e1)
12    e3 := fmt.Errorf("E3: %w", e2)
13    fmt.Println(errors.Unwrap(e3))
14    fmt.Println(errors.Unwrap(e2))
15    fmt.Println(errors.Unwrap(e1))
16}
1$ go run main.go
2E2: Error One happended
3Error One happended

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

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

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

1func Is(err, target error) bool
  • هر دو این اینترفیس‌ها به یک نوع تایپ منصوب شده باشند.
  • مقدار داخلی اینترفیس‌ها باید با هم برابر باشند یا اینکه هر دو (nil) باشند.
 1package main
 2import (
 3    "errors"
 4    "fmt"
 5)
 6type errorOne struct{}
 7func (e errorOne) Error() string {
 8    return "Error One happended"
 9}
10func main() {
11    var err1 errorOne
12    err2 := do()
13    if err1 == err2 {
14        fmt.Println("Equality Operator: Both errors are equal")
15    }
16    if errors.Is(err1, err2) {
17        fmt.Println("Is function: Both errors are equal")
18    }
19}
20func do() error {
21    return errorOne{}
22}
1$ go run main.go
2Equality Operator: Both errors are equal
3Is function: Both errors are equal