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 در پکیج برای بازکردن فایل استفاده کردیم. اگر دقت کنید این تابع ۲ تا خروجی دارد یکی ساختار 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 است و دقت کنید اگر این متغیر را به تابع دیگری پاس دهید قطعا با panic مواجه خواهید شد.

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