معالجة الأخطاء في Go لا تتم عبر الاستثناءات. بل إن الأخطاء قيم عادية من أنواع تُنفّذ واجهة error المدمجة. واجهة error بسيطة للغاية، فهي تحتوي على طريقة واحدة فقط هي Error() التي تُرجع رسالة الخطأ كسلسلة نصية.
type error interface {
Error() string
}
كلما عرّفت دالة قد يحدث فيها خطأ أثناء التنفيذ ويحتاج إلى الوصول إلى المستدعي، عليك تضمين error ضمن أنواع الإرجاع. وإذا كانت الدالة تُرجع عدة قيم، فبحسب الاصطلاح يكون error دائمًا آخرها.
func DoSomething() (int, error) {
// ...
}
لست مضطرًا إلى تنفيذ واجهة الخطأ بنفسك في كل مرة. لإنشاء خطأ بسيط، يمكنك استخدام الدالة errors.New() التي تنتمي إلى حزمة errors في المكتبة القياسية. الشيء الوحيد الذي عليك تمريره هو رسالة الخطأ كسلسلة نصية، وستتولى errors.New() إنشاء قيمة تحتوي على رسالتك وتُنفّذ واجهة error.
إذا أرجعت الدالة خطأ، فمن الممارسات الجيدة إرجاع القيمة الصفرية لبقية معاملات الإرجاع:
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
لا ينبغي أن تفترض أن جميع الدوال تُرجع قيمًا صفرية لبقية قيم الإرجاع عند وجود خطأ. من أفضل الممارسات أن تفترض أن استخدام أي من قيم الإرجاع الأخرى غير آمن إذا أُرجِع خطأ.
على سبيل المثال، تخيّل دالة كانت تحاول القراءة من ملف فأرجعت شريحة بايت نصف ممتلئة وخطأ. إذا أعدت استخدام شريحة البايت المُرجعة في الكود، مفترضًا أنها فارغة دائمًا لوجود خطأ، فقد تصادف bug خفية يصعب تتبعها.
الاستثناءات الوحيدة هي الحالات التي يوضح فيها توثيق الدالة صراحةً أن قيم الإرجاع الأخرى ذات معنى عند وجود خطأ. انظر إلى [توثيق 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.
لتجنب تداخل "المسار السعيد" في الكود، يُفضّل معالجة حالات الخطأ أولًا، عادةً عبر الإرجاع المبكر. راجع هذا المقال الشهير لمات راير لمزيد من التفاصيل.
// 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: "...",
}
}