Go Programming

تسلط بر مدیریت خطاها در Go: از بازگشت‌های ساده تا الگوهای پیشرفته

برخلاف زبان‌هایی مانند پایتون، جاوا یا جاوااسکریپت، Go بلوک‌های try/catch یا مکانیزم استثناهای داخلی ندارد. در عوض، Go فلسفه‌ای را می‌پذیرد که خطاها مقادیر هستند—شهروندان درجه اولی که باید به صراحت در نقطه وقوع مدیریت شوند. برای توسعه‌دهندگان متوسط تا پیشرفته، درک نحوه مدیریت مؤثر این خطاها برای ساخت برنامه‌های مقاوم، قابل نگهداری و قابل دیباگ حیاتی است. این پست سیر تحول مدیریت خطاها در Go را بررسی می‌کند و از الگوهای پایه به تکنیک‌های پیشرفته‌ای مانند بسته‌بندی خطاها و انواع خطای سفارشی حرکت می‌کند.

آناتومی یک خطا در Go

در هسته خود، نوع error در Go صرفاً یک رابط (interface) با یک روش است:

type error interface {
    Error() string
}

این سادگی هم نقطه قوت و هم پیچیدگی Go است. از آنجا که هر نوعی که این رابط را پیاده‌سازی کند می‌تواند به عنوان خطا بازگردانده شود، توسعه‌دهندگان انعطاف‌پذیری قابل توجهی دارند. با این حال، این بدان معناست که مدیریت خطاها اغلب نیاز به ارجاع نوع (type assertion)، بسته‌بندی و بررسی دقیق انواع خطاها قبل از تعیین استراتژی بازیابی دارد.

مدیریت خطای پایه و خطاهای ارجاعی (Sentinel Errors)

رایج‌ترین الگو، بررسی وجود خطای غیر-خالی (non-nil) بلافاصله پس از فراخوانی یک تابع است. اگرچه این کار ساده به نظر می‌رسد، اما مقایسه خطاها به صورت مستقیم با استفاده از errors.Is() ضروری است. در نسخه‌های قدیمی‌تر Go، توسعه‌دهندگان از «خطاهای ارجاعی» استفاده می‌کردند—متغیرهای خطای سراسری از پیش تعریف شده—برای نشان دادن شرایط شکست خاص.

var ErrNotFound = errors.New("record not found")

func FindUser(id int) (*User, error) {
    if id == 0 {
        return nil, ErrNotFound
    }
    // ... logic to find user
}

برای بررسی اینکه آیا یک خطا با یک خطای ارجاعی مطابقت دارد، همیشه از errors.Is() استفاده کنید و نه برابری مستقیم (==). این کار سازگاری با بسته‌بندی خطاها را تضمین می‌کند، موضوعی که در ادامه بررسی خواهیم کرد.

بسته‌بندی خطاها برای افزودن زمینه (Context)

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

Go 1.13 دستورالعمل %w را در بسته fmt معرفی کرد که به توسعه‌دهندگان امکان می‌دهد خطاها را بسته‌بندی کنند. این کار زنجیره خطای اصلی را حفظ کرده و لایه‌ای از زمینه را اضافه می‌کند.

func GetUser(id int) (*User, error) {
    user, err := db.GetUserByID(id)
    if err != nil {
        // Wrap the error with additional context
        return nil, fmt.Errorf("fetching user %d: %w", id, err)
    }
    return user, nil
}

هنگام بسته‌بندی خطاها، errors.Is() و errors.As() زنجیره خطاهای بسته‌بندی شده را پیمایش می‌کنند. این بدان معناست که شما می‌توانید نوع خطای زیرین خاص را حتی اگر چندین بار بسته‌بندی شده باشد، بررسی کنید.

انواع خطای سفارشی و errors.As()

گاهی اوقات، شما نیاز دارید خطاها را بر اساس نوع آن‌ها به صورت متفاوتی مدیریت کنید. برای مثال، ممکن است بخواهید برای یک خطای «یافت نشد» کد وضعیت 404 و برای یک خطای «اتصال پایگاه داده منقضی شده» کد وضعیت 500 بازگردانید. برای این کار، به انواع خطای سفارشی و تابع errors.As() نیاز دارید.

type TimeoutError struct {
    Operation string
    Duration  time.Duration
}

func (e *TimeoutError) Error() string {
    return fmt.Sprintf("%s operation timed out after %v", e.Operation, e.Duration)
}

func HandleRequest(w http.ResponseWriter, err error) {
    var timeoutErr *TimeoutError
    if errors.As(err, &timeoutErr) {
        http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
        return
    }
    // Handle other errors
    http.Error(w, "Internal server error", http.StatusInternalServerError)
}

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

بهترین شیوه‌ها برای کد تولید (Production Code)

  1. خطاها را نادیده نگیرید: حتی اگر انتظار خطا دارید، آن را مدیریت یا ثبت کنید. نادیده گرفتن خطاها منجر به شکست‌های خاموش می‌شود.
  2. در لایه مناسب بسته‌بندی کنید: خطاها را زمانی بسته‌بندی کنید که زمینه معناداری اضافه می‌کنید. خطاهایی را که بلافاصله بدون تغییر به کاربر بازگردانده می‌شوند، بسته‌بندی نکنید.
  3. برای بررسی‌ها از errors.Is استفاده کنید: همیشه errors.Is() را بر مقایسه مستقیم ترجیح دهید.
  4. پیام‌های خطا را مختصر نگه دارید: هنگام بازگرداندن خطاها به کاربران، جزئیات فنی که ممکن است اطلاعات پیاده‌سازی داخلی را فاش کنند، حذف کنید.

نتیجه‌گیری

مدیریت خطاها در Go درباره اجتناب از پیچیدگی نیست، بلکه درباره شفاف‌سازی آن است. با تسلط بر خطاهای ارجاعی، بسته‌بندی خطاها با fmt.Errorf و بررسی نوع با errors.As()، می‌توانید برنامه‌های Go بنویسید که نه تنها کاربردی، بلکه مقاوم و آسان برای دیباگ هستند. پیچیدگی ظاهری مدیریت خطاها در Go را بپذیرید؛ زیرا این کار در بلندمدت سود زیادی در نگهداری کدبیس شما خواهد داشت.

Share: