مشخصات بررسی خطا در گولانگ

انواع خطا

چندین گزینه برای اعلام خطا وجود دارد. قبل از انتخاب بهترین گزینه برای مورد استفاده خود، لازم است موارد زیر را مد نظر قرار دهید:

  • آیا فراخواننده نیاز دارد خطا را مطابقت دهد تا با آن برخورد کند؟ اگر بله، ما باید با اعلام متغیرهای خطای بالادست یا انواع سفارشی، توابع errors.Is یا errors.As را پشتیبانی کنیم.
  • آیا پیام خطا یک رشته ثابت است یا یک رشته پویا نیازمند اطلاعات زمینه‌ای است؟ برای رشته‌های ثابت، می‌توانیم از errors.New استفاده کنیم، اما برای حالت دوم باید از fmt.Errorf یا نوع خطای سفارشی استفاده کنیم.
  • آیا ما خطاهای جدید برگشتی از توابع پایین‌دست را منتقل می‌کنیم؟ اگر بله، به بخش پوشش خطاها مراجعه کنید.
مطابقت خطا؟ پیام خطا راهنمایی
خیر ثابت errors.New
خیر پویا fmt.Errorf
بله ثابت متغیر بالادستی با errors.New
بله پویا نوع error سفارشی

به عنوان مثال، برای نمایش خطاها با رشته‌های ثابت، از errors.New استفاده کنید. اگر فراخواننده نیاز دارد که این خطا را مطابقت داده و با آن برخورد کند، آنرا به عنوان یک متغیر صادر کنید تا با errors.Is مطابقت پیدا کند.

بدون مطابقت خطا

// بسته foo

func Open() error {
  return errors.New("نمی‌توان باز کرد")
}

// بسته bar

if err := foo.Open(); err != nil {
  // نمی‌توان با خطا برخورد کرد.
  panic("خطای ناشناخته")
}

مطابقت خطا

// بسته foo

var ErrCouldNotOpen = errors.New("نمی‌توان باز کرد")

func Open() error {
  return ErrCouldNotOpen
}

// بسته bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // با خطا برخورد کنید
  } else {
    panic("خطای ناشناخته")
  }
}

برای خطاهای دارای رشته‌های پویا از fmt.Errorf استفاده کنید اگر فراخواننده نیاز به مطابقت نداشته باشد. اگر فراخواننده واقعاً نیاز به مطابقت دارد، در آن صورت از یک error سفارشی استفاده کنید.

بدون مطابقت خطا

// بسته foo

func Open(file string) error {
  return fmt.Errorf("فایل %q یافت نشد", file)
}

// بسته bar

if err := foo.Open("testfile.txt"); err != nil {
  // نمی‌توان با خطا برخورد کرد.
  panic("خطای ناشناخته")
}

مطابقت خطا

// بسته foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("فایل %q یافت نشد", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// بسته bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // با خطا برخورد کنید
  } else {
    panic("خطای ناشناخته")
  }
}

توجه داشته باشید که اگر متغیرها یا انواع خطا را از یک بسته صادر کنید، آنها به عنوان بخشی از رابط برنامه‌نویسی عمومی بسته محسوب می‌شوند.

پوشش خطا

هنگامی که یک خطا در حین فراخوانی یک متد دیگر رخ می‌دهد، معمولاً سه روش برای رسیدگی به آن وجود دارد:

  • بازگرداندن خطا اصلی در حالت فعلی.
  • استفاده از fmt.Errorf با %w برای افزودن متن به خطا و سپس بازگردانی آن.
  • استفاده از fmt.Errorf با %v برای افزودن متن به خطا و سپس بازگردانی آن.

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

در غیر این صورت، باید تا جای ممکن متن اضافی به خطا اضافه کنید تا خطاهای گمراه‌کننده مانند "اتصال رد شد" رخ ندهد. به جای این موارد، شما خطاهای مفیدتری مانند "فراخوانی سرویس فو: اتصال رد شد" دریافت خواهید کرد.

از fmt.Errorf برای افزودن متن به خطاها استفاده کنید و بین %w و %v بر اساس اینکه فراخواننده باید قادر به تطبیق و استخراج علت اصلی باشد یا خیر، انتخاب کنید.

  • از %w استفاده کنید اگر فراخواننده باید دسترسی به خطای زیرین داشته باشد. این رفتار مناسبی برای اکثر خطاهای پیچیده است؛ اما باید به یاد داشت که فراخواننده ممکن است به این رفتار وابسته شود. بنابراین برای خطاهایی که متغیرها یا انواع مشخصی دارند، آن‌ها را به عنوان قرارداد تابع ضبط و آزمایش کنید.
  • از %v برای مسدود کردن خطای زیرین استفاده کنید. فراخواننده نمی‌تواند آن را مطابقت دهد، اما در آینده می‌توانید به %w تغییر دهید.

زمانی که متن به خطای بازگردانی‌شده اضافه می‌شود، از جملاتی مانند "ناموفق در" برای حفظ متن کوتاه خودداری کنید. هنگامی که خطا از طریق پشته نفوذ می‌کند، به طور لایه به لایه بر روی هم قرار خواهد گرفت:

پیشنهاد نمی‌شود:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}

// failed to x: failed to y: failed to create new store: the error

توصیه می‌شود:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
// x: y: new store: the error

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

نام گذاری اشتباه

برای مقادیر خطای ذخیره‌شده به عنوان متغیرهای سراسری، پیشوند Err یا err را براساس اینکه آیا آن‌ها صادر شده‌اند استفاده کنید. لطفا به دستورالعملها مراجعه کنید. برای ثابت‌ها و متغیرهای سراسری غیرصادر شده، از زیرخط (_) به عنوان پیشوند استفاده کنید.

var (
  // دو خطای زیر را صادر کنید تا کاربران این بسته بتوانند آن‌ها را با استفاده از errors.Is مطابقت دهند.
  ErrBrokenLink = errors.New("پیوند شکسته است")
  ErrCouldNotOpen = errors.New("نمی‌توان باز کرد")

  // این خطا صادر نیست زیرا نمی‌خواهیم که بخشی از رابط عمومی ما باشد. با این حال، هنوز می‌توانیم آن را در داخل بسته با errors استفاده کنیم.
  errNotFound = errors.New("یافت نشد")
)

برای انواع خطای سفارشی، پسوند Error را استفاده کنید.

// به همین ترتیب، این خطا صادر می‌شود تا کاربران این بسته بتوانند آن را با errors.As مطابقت دهند.
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("فایل %q یافت نشد", e.File)
}

// این خطا صادر نیست زیرا نمی‌خواهیم که بخشی از رابط عمومی باشد. با این حال، هنوز می‌توانیم آن را در داخل بسته با errors.As استفاده کنیم.
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("رفع %q", e.Path)
}

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

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

این شامل اما به آن محدود نمی‌شود:

  • تطبیق خطا با errors.Is یا errors.As اگر فراخوانی‌کننده بر روی تعریف خاصی از خطا توافق کرده باشد و مدیریت شاخه‌ها به روش‌های مختلف
  • ثبت خطا و به شکل مناسب کاهش آرام فراگیر اگر خطای قابل اصلاح است
  • بازگرداندن یک خطای تعریف شده‌ی خوب اگر وضعیت یک خطا مشخص‌کننده‌ی یک شرایط خرابی خاص در دامنه باشد
  • بازگرداندن خطا، آیا بسته بندی شده یا به صورت مستقیم

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

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

نامناسب: ثبت خطا و بازگرداندن آن

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

u, err := getUser(id)
if err != nil {
  // نامناسب: برای کسر حاصل از جواب، شرح را بخوانید
  log.Printf("کاربر %q را نتوانستم بیاورم: %v", id, err)
  return err
}

مناسب: بسته بندی خطا و بازگرداندن آن

خطاهای بالاتر از پشته این خطا را خواهند مدیریت کرد. استفاده از %w اطمینان می‌یابد که آن‌ها می‌توانند خطا را با errors.Is یا errors.As مطابقت دهند اگر مرتبط باشد.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("بیاوری کاربر %q: %w", id, err)
}

مناسب: ثبت خطا و کاهش آرام فراگیر

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

if err := emitMetrics(); err != nil {
  // شکست در نوشتن معیارها نباید
  // برنامه را خراب کند.
  log.Printf("نتوانستم معیارها را منتشر کنم: %v", err)
}

مناسب: تطابق خطا و کاهش آرام فراگیر به صورت مناسب

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

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // کاربر وجود ندارد. از UTC استفاده کنید.
    tz = time.UTC
  } else {
    return fmt.Errorf("بیاوری کاربر %q: %w", id, err)
  }
}

رسیدگی به شکست‌های ادعاها

ادعای انواع با یک مقدار بازگشتی تنها، در صورت تشخیص نادرست نوع، واکنش ناگهانی خواهد داشت. بنابراین، همیشه از "ویرگول، اوک" استفاده کنید.

توصیه نشده:

t := i.(string)

توصیه می‌شود:

t, ok := i.(string)
if !ok {
  // خطا را به شکل مناسب مدیریت کنید
}

اجتناب از استفاده از panic

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

توصیه نمی‌شود:

func run(args []string) {
  if len(args) == 0 {
    panic("یک آرگومان مورد نیاز است")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

توصیه می‌شود:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("یک آرگومان مورد نیاز است")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

استفاده از panic/recover یک استراتژی برخورد با خطا نیست. تنها باید در صورت رخداد یک رویداد غیرقابل بازیابی (مانند ارجاع به nil) panic کند. یک استثنا در هنگام مقدمات برنامه است: مواردی که باعث panic شدن برنامه می‌شوند باید در زمان راه‌اندازی برنامه مدیریت شوند.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

حتی در کد آزمایشی، استفاده از t.Fatal یا t.FailNow به جای panic، برای اطمینان از علامت‌گذاری شکست‌ها، ترجیح‌داده می‌شود.

توصیه نمی‌شود:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("ناتوان در تنظیم آزمایش")
}

توصیه می‌شود:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("ناتوان در تنظیم آزمایش")
}