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")
}
نباید فرض کنید که اگر خطایی وجود دارد، همهی توابع برای سایر مقادیر بازگشتی مقدار صفر برمیگردانند. بهتر است فرض کنید اگر خطایی رخ داده باشد، استفاده از هیچیک از مقادیر بازگشتی دیگر امن نیست. تنها استثنا مواردی است که مستندات به روشنی بگوید سایر مقادیر بازگشتی در صورت وجود خطا معنا دارند.
اگر میخواهید چنین خطای سادهای را در چند جا استفاده کنید، به جای اینکه 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
}
اگر تابعی را فراخوانی کنید که خطا برمیگرداند، معمولاً مقدار خطا را در متغیری به اسم err نگه میدارند.
پیش از استفاده از نتیجهی واقعی تابع، باید بررسی کنید که خطایی وجود نداشته باشد.
برای اینکه «مسیر موفق» کدتان تودرتو نشود، بهتر است ابتدا حالتهای خطا را مدیریت کنید.
میتوانیم با == و != خطا را با nil مقایسه کنیم؛ وقتی err برابر nil نباشد، میدانیم خطایی رخ داده است.
func processUserFile() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
بیشتر اوقات، خطا همانطور که در مثال بالا دیدید در پشتهی توابع به بالا برگردانده میشود. راه دیگر مدیریت خطا این است که آن را لاگ کنید و کار دیگری را ادامه دهید. بهتر است خطا را یا برگردانید یا لاگ کنید، نه هر دو.
چون بیشتر توابع Go خطا را بهعنوان یکی از مقادیر بازگشتی دارند، الگوی if err != nil را همهجای کد Go میبینید و به کار میبرید.
اگر میخواهید خطایتان اطلاعات بیشتری از فقط پیام خطا داشته باشد، میتوانید یک نوع خطای سفارشی بسازید.
همانطور که پیشتر گفته شد، هر چیزی که رابط error را پیادهسازی کند (یعنی متد Error() string داشته باشد) میتواند در Go بهعنوان خطا به کار برود.
معمولاً برای ساختن نوع خطای سفارشی از یک struct استفاده میشود.
طبق قرارداد، اسم نوعهای خطای سفارشی باید با 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: "...",
}
}
روزی که مدتها منتظرش بودید بالاخره فرا رسید و حالا با افتخار صاحب یک مزرعهی زیبا در کوههای آلپ هستید.
هنوز هم دوست ندارید صبح زود برای غذا دادن به گاوهایتان از خواب بیدار شوید.
چون مهندس زبردستی هستید، یک دستگاه پخش غذا به اسم FEED-M-ALL میسازید.
آخرین چیزی که برای به پایان رساندن پروژهتان لازم دارید، تکهکدی است که مقدار علوفهای را که هر گاو باید دریافت کند محاسبه میکند. مهم است که هر گاو مقدار یکسانی دریافت کند؛ باید از بروز درگیری جلوگیری کنید. گاوها بسیار حساساند.
خوشبختانه لازم نیست همهی فرمولهای محاسبهی مقدار علوفه را خودتان به دست آورید. از یک کتابخانهی خارجی مرموز استفاده میکنید که در اینترنت پیدایش کردهاید. قرار است نتیجهاش شادترین گاوها باشد. این کتابخانه نوعی را ارائه میدهد که واسط زیر را پیادهسازی میکند. در کدی که خودتان مینویسید به این تکیه خواهید کرد.
type FodderCalculator interface {
FodderAmount(int) (float64, error)
FatteningFactor() (float64, error)
}
همانطور که روی کدتان کار میکنید، مدیریت خطا را بهتر میکنید تا مقاومتر شود و بعدها که در زندگی روزمرهی مزرعه از آن استفاده میکنید، debug کردنش آسانتر باشد.
اول از همه، روی نوشتن کدی تمرکز میکنید که برای محاسبهی مقدار علوفهی هر گاو لازم است.
تابعی به اسم DivideFood پیادهسازی کنید که یک FodderCalculator و تعداد گاوها بهصورت عدد صحیح را به عنوان آرگومان میپذیرد.
برای این کار، فرض کنید تعداد گاوهای ورودی همیشه بزرگتر از صفر است.
تابع باید مقدار غذای هر گاو را بهصورت float64 برگرداند، یا اگر خطایی رخ داد، خود آن خطا را برگرداند.
برای انجام محاسبه، ابتدا باید مقدار کل علوفهی همهی گاوها را به دست آورید.
این کار با فراخوانی متد FodderAmount و پاس دادن تعداد گاوها انجام میشود.
علاوه بر این، به ضریبی نیاز دارید که این مقدار باید در آن ضرب شود.
این ضریب را با فراخوانی متد FatteningFactor به دست میآورید.
با این دو مقدار و تعداد گاوها، حالا میتوانید مقدار غذای هر گاو را (بهصورت float64) محاسبه کنید.
همین مقدار باید از تابع DivideFood برگردانده شود.
اگر یکی از متدهایی که فراخوانی میکنید خطایی برگرداند، اجرا باید متوقف شود و همان خطا (بدون تغییر) از تابع DivideFood برگردانده شود.
// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>
// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"
در حین انجام کار اول بالا، متوجه شدید کتابخانهی خارجیای که استفاده میکنید آنقدرها هم که فکر میکردید باکیفیت نیست. مثلاً نمیتواند ورودیهای نامعتبر را درست مدیریت کند. میخواهید این محدودیت را با افزودن یک بررسی برای مقدار ورودی در کد خودتان دور بزنید.
تابعی به اسم ValidateInputAndDivideFood بنویسید که همان امضای DivideFood بالا را دارد.
DivideFood را فراخوانی کند و نتیجهی آن فراخوانی را برگرداند."invalid number of cows" برگرداند.ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>
ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"
بررسی تعداد گاوها پیش از پاس دادنش کار خوبی بود، اما از پیام خطای غیراختصاصی چندان راضی نیستید.
تصمیم میگیرید بهتر عمل کنید و یک نوع خطای سفارشی به اسم InvalidCowsError بسازید.
این خطای سفارشی باید تعداد گاوها (int) و یک پیام سفارشی (string) را نگه دارد، و متد Error باید دادهها را در قالب زیر سریالایز کند:
{number of cows} cows are invalid: {custom message}
با در دست داشتن خطای سفارشیتان، تابعی به اسم ValidateNumberOfCows پیادهسازی کنید که تعداد گاوها را بهصورت عدد صحیح میپذیرد و یک خطا (یا nil) برمیگرداند.
InvalidCowsError برمیگرداند که پیام سفارشیاش "there are no negative cows" تنظیم شده است.InvalidCowsError برمیگرداند که پیام سفارشیاش "no cows don't need food" تنظیم شده است.nil برمیگرداند تا نشان دهد اعتبارسنجی موفق بوده است.err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"
بعد از کار سخت راهاندازی این تابع اعتبارسنجی، متوجه میشوید که دیگر غروب شده است؛ از میزتان بلند میشوید تا از تماشای غروب خورشید روی کوهها لذت ببرید. و کارِ واقعاً اضافه کردن تابع اعتبارسنجی جدید به کدتان را به روزی دیگر موکول میکنید.