چرا برای هندل‌کردن خطاها در گولنگ از RichError استفاده کنیم؟

توی این پست می خوام که در مورد اینکه چرا استفاده از پکیجی مثل Rich Error به ما کمک می کنه که بتونیم سرویس‌های گولنگیمونو راحت تر دیباگ کنیم توضیح بدم.

قبل از هر چیزی پیشنهاد می کنم که این سخنرانی گوفرکانف در سال ۲۰۱۹ راجع به Rich Error رو ببینید.

https://www.youtube.com/watch?v=4WIhhzTTd0Y




شاید براتون سوال پیش بیاد که توی گو هندل کردن خطاها به خودی خود به خوبی داره انجام میشه و چرا اصلا نیاز هست که وقت بیشتری براش بزاریم. در جواب باید بگم که یکسری سوال هست که با ارور هندلینگ عادی نمیشه بهشون جواب داد و به نظر من به اگه هنوز با این سوال ها مواجه نشدید شاید این پکیج خیلی به کارتون نیاد.

حالا اون سوال ها چی هستن؟

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

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

خب قبل از اینکه بریم سراغ جواب این سوال ها باید بگم که هندل کردن خطاها توی گو می تونه به صورت panic & recover یا به صورت شرطی باشه. من اینجا هیچ کاری با حالت اول ندارم و فقط تمرکزم بر روی حالت شرطی هست و بحث راجع به اینکه کدوم حالت مناسب‌تر هست بستگی به خیلی شرایط داره که خارج از موضوع این مطلب هست.

پروسه ای که منجر به این خطا شده از کجا شروع شده و بعد از طی کردن چه مراحلی به اینجا رسیده؟

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

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

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

در هنگامی که خطا رخ داده ما چه runtime argument هایی داشتیم؟

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

یه مثال خیلی ساده بخوام بزنم مثلا شما یه قسمت ثبت نام دارید که کاربرها باید بیان و شماره موبایلشون رو وارد کنن و یکسری فرمت‌های خاص رو شما کاور نکردید، مثلا اگه کاربر شمارشو به صورت +۹۸۹..... وارد کنه اون فانکشن خطا میده. اینجا هست که ما وقتی داریم خطا رو می‌سازیم (یا وقتی chain می کنیم) می تونیم به اون یکسری meta data اضافه کنیم.

آیا لازم هست که این خطا رو لاگ کنیم؟

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

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

  • نوع Invalid که معمولا نشان دهنده‌ای این هست که اطلاعات ورودی صحیح نیست.
  • نوع Forbidden که همونطور که از اسمش مشخصه نشانگر این هست که کاربر اجازه دسترسی نداره.
  • نوع Notfound که این هم از اسمش مشخصه و مثلا توی اون مثالی که بالا زدم وقتی استفاده میشه که نتیجه ای وجود نداشته باشه.
  • نوع Unexcpected: که برای مواقعی قرار میگیره که خطای غیرمنتظره‌ای رخ میده و اگه ما هنگام ساخت خطا نوعی واسش در نظر نگیریم این نوع براش انتخاب میشه.
  • نوع Unavailable که معمولا در رابطه با سرویس های خارجی بیشتر رخ میده.

این رو هم فراموش نکنیم که این دسته بندی خیلی عمومی هست و شاید بنا به نیاز هر اپی شاید کم یا زیاد بشه.

حالا اگه خطایی که رخ میده از نوع notfound باشه شاید ما فقط به کاربر یه ریسپانس نشون بدیم و اصلا نیازی نباشه که اون رو لاگ کنیم.

ریسپانس مناسب وقتی که این خطا رخ میده با توجه به محیط اجرا باید چی باشه؟

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

یه مدل از خروجی:

لاگ یک خطای unexpected
لاگ یک خطای unexpected

اینجا من کمی راجع به این عکس توضیح میدم، گرچه تا حدودی گویا هست. اینطور که مشخصه این خطا توسط یک کرون جاب ران شده که سرویس notifier بوده و بعد بخاطر خطایی که توی مرحله بعد خورده در تلاش چهارم نتونسته که اون کار رو انجام بده. خطایی هم که بوده این بوده که نتونسته به دیتابیس وصل بشه چون دیتابیس دان بوده. شروع خطا هم در خط۲۳۱ از فایل مربوطه بوده! :)



شاید براتون سوال پیش بیاد که چرا این پکیج از اینترفیس اصلی خطا در گولنگ تبعیت (implement) نمی کنه؟

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




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

https://github.com/p3ym4n/re