خط

خطا در Go

2 تمرین

درباره‌ی خطا

رابط error

مدیریت خطا در Go با استثناها انجام نمی‌شود. در عوض، خطاها مقدارهای عادی از نوع‌هایی هستند که رابط error توکار را پیاده‌سازی می‌کنند. رابط error بسیار کمینه است. تنها یک متد Error() دارد که پیام خطا را به‌صورت رشته برمی‌گرداند.

type error interface {
  Error() string
}

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

func DoSomething() (int, error) {
  // ...
}

ساختن و بازگرداندن یک خطای ساده

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

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

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

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

برای مثال، تصور کنید تابعی در حال خواندن از یک فایل باشد و یک اسلایس بایتی نیمه‌پرشده به همراه یک خطا برگرداند. اگر آن اسلایس بایتی بازگشتی را در کد خود دوباره به کار ببرید و فرض کنید چون خطایی رخ داده همیشه خالی است، ممکن است به باگ‌های ظریفی بربخورید.

تنها استثناها مواردی هستند که مستندات تابع به‌صراحت بیان می‌کند که سایر مقادیر بازگشتی در صورت وجود خطا معنادار هستند. برای نمونه، به [مستندات Write روی یک بافر][buffer-write] نگاه کنید.

مطمئن شوید پیام خطایی که می‌دهید تا حد امکان مشخص است، چون خطاها به‌طور پیش‌فرض شامل هیچ استک‌تریسی نیستند. طبق قرارداد، پیام خطا باید با حرف کوچک شروع شود و با نقطه پایان نیابد.

اگر می‌خواهید از چنین خطایی در چند جا استفاده کنید (یا می‌خواهید خطا را در اختیار مصرف‌کننده‌ی بسته‌ی خود بگذارید)، بهتر است به‌جای استفاده‌ی درجایی از errors.New، یک متغیر برای خطا تعریف کنید. طبق قرارداد، اسم متغیر باید با Err یا err شروع شود (بسته به اینکه صادرشده باشد یا نه). به این متغیرهای خطا اغلب خطاهای نگهبان می‌گویند.

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

برای اینکه نشان دهید در جریان اجرای تابع خطایی رخ نداده، nil را به‌عنوان خطا برگردانید:

func Foo() (int, error) {
  return 10, nil
}

گنجاندن متغیرها در پیام خطا

اگر لازم است پیام خطای پیچیده‌تری از چند متغیر بسازید، می‌توانید از قالب‌بندی رشته استفاده کنید. Go برای این کار تابع ویژه‌ی fmt.Errorf را دارد. این تابع رشته‌ی پیام خطا را می‌سازد و یک خطا برمی‌گرداند.

input := 123
action := "UPDATE"

err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"

بررسی خطا

اگر تابعی را فراخوانی کنید که خطا برمی‌گرداند، معمول است که مقدار خطا را در متغیری به اسم err ذخیره کنید. پیش از استفاده از نتیجه‌ی واقعی تابع، باید بررسی کنید که خطایی رخ نداده باشد. می‌توانیم از == و != برای مقایسه‌ی خطا با nil استفاده کنیم؛ اگر err برابر nil نباشد، می‌دانیم خطایی رخ داده است.

برای پرهیز از تودرتو کردن «مسیر خوش» کد، باید ابتدا موارد خطا مدیریت شوند، معمولاً با یک بازگشت زودهنگام. برای جزئیات بیشتر، این مقاله‌ی معروف از Mat Ryer را ببینید.

// Do this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    return err
  }
  // do something with file
}

// Not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    // handle err
  } else {
    // do something with file
  }
}

// Also not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err == nil {
    // do something with file
  }
  // handle err
}

بیشتر اوقات، خطا همان‌طور که در مثال اول بالا نشان داده شد، در پشته‌ی توابع به بالا بازگردانده می‌شود. روش دیگری برای مدیریت خطا می‌تواند این باشد که خطا را لاگ کنید و با عملیات دیگری ادامه دهید. رویه‌ی خوبی است که خطا را یا بازگردانید یا لاگ کنید، نه هر دو.

چون بیشتر توابع در Go خطا را به‌عنوان یکی از مقادیر بازگشتی دارند، الگوی if err != nil را همه‌جای کد Go خواهید دید و به کار خواهید برد.

نوع‌های خطای سفارشی

اگر می‌خواهید خطای شما اطلاعاتی بیش از فقط رشته‌ی پیام خطا داشته باشد، می‌توانید یک نوع خطای سفارشی بسازید. همان‌طور که پیش‌تر گفته شد، هر مقداری که رابط error را پیاده‌سازی کند (یعنی متد Error() string داشته باشد) می‌تواند در Go به‌عنوان یک خطا عمل کند.

معمولاً برای ساختن نوع خطای سفارشی از یک ساختار استفاده می‌شود. طبق قرارداد، اسم نوع‌های خطای سفارشی باید با Error پایان یابد. همچنین بهتر است متد Error() string را با گیرنده‌ی اشاره‌گر تعریف کنید؛ برای اطلاع از دلیل آن، این نظر Stackoverflow را ببینید. توجه کنید که این یعنی باید اشاره‌گری به خطای سفارشی خود برگردانید، وگرنه به‌عنوان error به حساب نمی‌آید، چون مقدار غیراشاره‌گری متد Error() string را فراهم نمی‌کند.

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}
ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

خطا را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین خطا قفل 4 تمرین دیگر را باز کنید