اذهب إلى المحتوى

كيفية إرفاق معلومات إضافية عن الأخطاء في لغة جو


هدى جبور

عندما تفشل دالة في لغة جو، فإنها تُعيد قيمةً باستخدام الواجهة error للسماح للمُستدعي بمعالجة الخطأ. في كثير من الأحيان يستخدم المطورون الدالة fmt.Errorf من الحزمة fmt لإعادة هذه القيم. قبل الإصدار 1.13 من لغة جو، كان الجانب السلبي لاستخدام هذه الدالة هو أننا سنفقد المعلومات المتعلقة بالأخطاء. لحل هذه المشكلة استخدم المطورون حزمًا توفر أساليب لتغليف wrap هذه الأخطاء داخل أخطاء أخرى، أو إنشاء أخطاء مخصصة من خلال تنفيذ التابع Error() string على أحد أنواع خطأ struct الخاصة بهم. أحيانًا، يكون إنشاء هذه الأنواع من struct مملًا إذا كان لديك عدد من الأخطاء التي لا تحتاج إلى المعالجة الصريحة من قبل من المُستدعي.

جاء إصدار جو 1.13 مع ميزات لتسهيل التعامل مع هذه الحالات، وتتمثل إحدى الميزات في القدرة على تغليف الأخطاء باستخدام دالة fmt.Errorf بقيمة خطأ error يمكن فكها لاحقًا للوصول إلى الأخطاء المغلفة. بالتالي، أنهى تضمين دالة تغليف للأخطاء داخل مكتبة جو القياسية الحاجة إلى استخدام طرق ومكتبات خارجية. إضافةً إلى ذلك، تجعل الدالتان errors.Is و errors.As من السهل تحديد ما إذا كان خطأ محدد مُغلّف في أي مكان داخل خطأ ما، ويمنحنا الوصول إلى هذا الخطأ مباشرةً دون الحاجة إلى فك جميع الأخطاء يدويًا.

سننشئ في هذا المقال برنامجًا يستخدم هذه الدوال لإرفاق معلومات إضافية مع الأخطاء التي تُعاد من الدوال، ثم سننشئ بنية struct للأخطاء المخصصة والتي تدعم دوال التغليف وفك التغليف.

المتطلبات

لتتابع هذا المقال، يجب أن تستوفي الشروط التالية:

إعادة ومعالجة الأخطاء في لغة جو

تُعد معالجة الأخطاء من الممارسات الجيدة التي تحدث أثناء تنفيذ البرامج حتى لا يراها المستخدمون أبدًا، لكن لا بُد من التعرّف عليها أولًا لمعالجتها. يمكننا في لغة جو معالجة الأخطاء في البرامج من خلال إعادة معلومات متعلقة بهذه الأخطاء من الدوال التي حدث فيها الخطأ باستخدام واجهة interface من نوع خاص هو error، إذ يسمح استخدام هكذا واجهة لأي نوع بيانات في لغة جو أن يُعاد مثل قيمة من نوع error طالما أن ذلك النوع لديه تابع Error() string معرّف. توفر مكتبة جو القياسية دوالًا، مثل fmt.Errorf للتعامل مع هذا الأمر وإعادة خطأ error.

سننشئ في هذا المقال برنامجًا مع دالة تستخدم fmt.Errorf لإعادة خطأ error، وسنضيف أيضًا معالج خطأ للتحقق من الأخطاء التي يمكن أن ترجعها الدالة. يمكنك العودة للمقال معالجة الأخطاء في لغة جو Go .

لدى معظم المطورين مجلد يضعون داخله مشاريعهم، وسنستخدم في هذا المقال مجلدًا باسم "projects". لننشئ هذا المجلد وننتقل إليه:

$ mkdir projects
$ cd projects

سننشئ داخل مجلد "projects" مجلدًا باسم "errtutorial" لوضع البرنامج داخله:

$ mkdir errtutorial

سننتقل إليه:

$ cd errtutorial

سنستخدم الآن الأمر go mod init لإنشاء وحدة errtutorial:

$ go mod init errtutorial

نفتح الآن ملف "main.go" من مجلد errtutorial باستخدام محرر نانو nano أو أي محرر آخر تريده:

$ nano main.go

الآن، سنكتب البرنامج، الذي سيمر ضمن حلقة على الأرقام من 1 إلى 3 ويحاول أن يحدد ما إذا كان أحد هذه الأرقام صالحًا أم لا باستخدام دالة تُدعى validateValue؛ فإذا لم يكن الرقم صالحًا، سيستخدم البرنامج الدالة fmt.Errorf لتوليد قيمة من النوع error تُعاد منها، إذ تسمح هذه الدالة بإنشاء قيمة error، بحيث تكون رسالة الخطأ هي الرسالة التي تقدمها للدالة، وهي تعمل بطريقة مشابهة للدالة fmt.Printf، لكن تُعاد مثل خطأ بدلًا من طباعة الرسالة على الشاشة.

الآن، ستكون هناك عملية تحقق من قيمة الخطأ في الدالة الرئيسية main، لمعرفة ما إذا كانت القيمة هي nil أو لا؛ فإذا كانت nil ستكون الدالة نجحت في التنفيذ دون أخطاء وسنرى الرسالة !valid، وإلا سيُطبع الخطأ الحاصل.

لنضع الآن هذه الشيفرة داخل ملف "main.go":

package main

import (
    "fmt"
)

func validateValue(number int) error {
    if number == 1 {
        return fmt.Errorf("that's odd")
    } else if number == 2 {
        return fmt.Errorf("uh oh")
    }
    return nil
}

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := validateValue(num)
        if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

تأخذ الدالة validateValue في البرنامج السابق رقمًا وتعيد خطأً اعتمادًا على ما إذا كانت القيمة المُمررة صالحة أم لا. في مثالنا العدد 1 غير صالح، وبالتالي تُعيد الدالة خطأ مع رسالة that's odd. الرقم 2 غير صالح ويؤدي إلى إرجاع خطأ مع رسالة uh oh. تستخدم الدالة validateValue الدالة fmt.Errorf لتوليد قيمة الخطأ المُعادة، إذ تُعد الدالة fmt.Errorf ملائمة لإعادة الأخطاء، لأنها تسمح بتنسيق رسالة خطأ باستخدام تنسيق مشابه للدالة fmt.Printf أو fmt.Sprintf دون الحاجة إلى تمرير هذه السلسلة string إلى errors.New.

ستبدأ حلقة for داخل الدالة main بالمرور على الأرقام من 1 إلى 3 وتخزّن القيمة ضمن المتغير num. سيؤدي استدعاء fmt.Printf داخل متن الحلقة إلى طباعة الرقم الذي يتحقق منه البرنامج حاليًا. بعد ذلك، سيجري استدعاء الدالة validateValue مع تمرير المتغير num (الذي نريد التحقق من صلاحيته)، وتخزين نتيجة الاستدعاء هذا في المتغير err. أخيرًا، إذا كانت قيمة err ليست nil، فهذا يعني أن خطًأ ما قد حدث وستُطبع رسالة الخطأ باستخدام fmt.Println، أما إذا كانت قيمته nil، فهذا يعني أن الرقم صالح وسنرى على الخرج "!valid".

بعد حفظ التغييرات الأخيرة يمكننا تشغيل ملف البرنامج "main.go" باستخدام الأمر go run من المجلد "errtutorial":

$ go run main.go

سيُظهر الخرج أن البرنامج قد تحقق من صحة كل رقم، وأن الرقم 1 والرقم 2 أديا إلى إظهار الأخطاء المناسبة لهما:

validating 1... there was an error: that's odd
validating 2... there was an error: uh oh
validating 3... valid!

نلاحظ أن البرنامج يحاول التحقق من الأرقام الثلاثة من خلال الدالة validateValue؛ ففي المرة الأولى قد حصل على قيمة غير صالحة هي 1 فطبع رسالة الخطأ that's odd؛ وحصل في المرة الثانية على قيمة غير صالحة أيضًا هي 2 فطبع رسالة خطأ مختلفة هي uh oh؛ وحصل في المرة الثالثة على الرقم 3 وهي قيمة صالحة فأعاد قيمة !valid وفي هذه الحالة تكون قيمة الخطأ المُعادة هي nil إشارةً إلى عدم حدوث أي مشاكل وأن الرقم صالح. وفقًا للطريقة التي كُتبت فيها الدالة validateValue، يمكننا أن نقول أنها ستُعيد nil من أجل أي قيمة باستثناء الرقمين 1 و2.

استخدمنا في هذا القسم الدالة fmt.Errorf لإنشاء قيم خطأ مُعادة من دالة، وأضفنا مُعالج خطأ يطبع رسالة الخطأ عند حصوله من الدالة. قد يكون من المفيد أحيانًا معرفة معنى الخطأ، وليس مجرد حدوث خطأ. سنتعلم في القسم التالي كيفية تخصيص معالجة الأخطاء لحالات محددة.

معالجة أخطاء محددة باستخدام أخطاء الحارس Sentinel errors

عندما نستقبل قيمة خطأ من دالة، فإن أبسط طريقة لمعالجة هذا الخطأ هي التحقق من قيمته إذا كانت nil أو لا، إذ يخبرنا هذا ما إذا كانت الدالة تتضمن خطأً، ولكن قد نرغب أحيانًا في تخصيص معالجة الأخطاء لحالة خطأ محددة. لنتخيل أن لدينا شيفرة مرتبطة بخادم بعيد، وأن معلومات الخطأ الوحيدة التي نحصل عليها هي "لديك خطأ". قد نرغب في معرفة ما إذا كان الخطأ بسبب عدم توفر الخادم، أو إذا كانت بيانات اعتماديات الاتصال connection credentials الخاصة بنا غير صالحة. إذا كنا نعلم أن الخطأ يعني أن بيانات اعتماد المستخدم خاطئة، فقد نرغب في إعلام المستخدم فورًا بذلك، ولكن إذا كان الخطأ يعني أن الخادم غير متاح، فقد نرغب في محاولة إعادة الاتصال عدة مرات قبل إعلام المستخدم. سنتمكن من خلال تحديد الاختلاف بين هذه الأخطاء من كتابة برامج أكثر قوة وسهولة في الاستخدام.

تتمثل إحدى الطرق التي يمكن من خلالها التحقق من نوع محدد من الخطأ في استخدام التابع Error على النوع error للحصول على الرسالة من الخطأ ومقارنة هذه القيمة بنوع الخطأ الذي نبحث عنه. لنتخيل أننا نريد إظهار رسالة غير الرسالة there was an error: uh oh التي رأيناها سابقًا عند الحصول على الخطأ uh oh، وإحدى الطرق لمعالجة هذه الحالة هي التحقق من القيمة المعادة من التابع Error كما يلي:

if err.Error() == "uh oh" {
    // Handle 'uh oh' error.
    fmt.Println("oh no!")
}

سيجري في الشيفرة أعلاه التحقق من قيمة السلسلة المُعادة من ()err.Error لمعرفة ما إذا كانت القيمة هي uh oh وستجري الأمور على نحو سليم، لكن لن تعمل الشيفرة السابقة إذا كانت السلسلة النصية التي تُعبّر عن الخطأ uh oh مختلفة قليلًا في مكان آخر في البرنامج.

يمكن أن يؤدي التحقق من الأخطاء بهذه الطريقة أيضًا إلى تحديثات مهمة على الشيفرة إذا كانت رسالة الخطأ نفسها بحاجة إلى التحديث، إذ يجب تحديث كل مكان يجري فيه التحقق من الخطأ. خذ على سبيل المثال الشيفرة التالية:

func giveMeError() error {
    return fmt.Errorf("uh h")
}

err := giveMeError()
if err.Error() == "uh h" {
    // "uh h" error code
}

تتضمن رسالة الخطأ هنا خطأً إملائيًا، إذ يغيب الحرف oعن السلسلة uh oh. إذا حدث ولاحظنا هذا الخطأ وأردنا إصلاحه، يتعين علينا إصلاحه في جميع أجزاء الشيفرة الأخرى التي تتضمن جملة التحقق "err.Error() == "uh oh، وإذا نسينا أحدهم وهذا محتمل لأنه تغيير في حرف واحد فقط، فلن يعمل معالج الخطأ المخصص لأنه يتوقع uh h وليس uh oh. قد نرغب في مثل هذه الحالات بمعالجة خطأ محدد بطريقة مختلفة، إذ من الشائع إنشاء متغير يكون الغرض منه الاحتفاظ بقيمة خطأ. يمكن للشيفرة بهذه الطريقة أن تتحقق من حصول هذا المتغير بدلًا من السلسلة. تبدأ أسماء هذه المتغيرات عادةً بالعبارة Err أو err للإشارة إلى أنها أخطاء. استخدم err، إذا كان الهدف استخدام الخطأ فقط داخل الحزمة المعرّف فيها، أما إذا أردت استخدامه خارج الحزمة، استخدم البادئة Err ليصبح قيمة مُصدّرة exported value على غرار ما نفعله مع الدوال أو البنى struct.

لنفترض الآن أنك كنت تستخدم إحدى قيم الخطأ هذه في المثال السابق الذي تضمن أخطاء إملائية:

var errUhOh = fmt.Errorf("uh h")

func giveMeError() error {
    return errUhOh
}

err := giveMeError()
if err == errUhOh {
    // "uh oh" error code
}

جرى هنا تعريف المتغير errUhOh على أنه قيمة الخطأ للخطأ "uh oh" (على الرغم من أنه يحتوي على أخطاء إملائية). تُعيد الدالة giveMeError قيمة errUhOh لأنها تريد إعلام المُستدعي بحدوث خطأ uh oh. تقارن شيفرة معالجة الخطأ بعد ذلك قيمة err التي أُعيدت من giveMeError مع errUhOh لمعرفة ما إذا كان الخطأ "uh oh" هو الخطأ الذي حدث.

حتى إذا جرى العثور على الخطأ الإملائي وجرى إصلاحه، فستظل جميع التعليمات البرمجية تعمل، لأن التحقق من الخطأ يجري من خلال المقارنة مع القيمة errUhOh، وقيمة errUhOh هي قيمة ثابتة تُعاد من giveMeError.

تُعرّف قيمة الخطأ المُراد فحصها ومقارنتها بهذه الطريقة بخطأ الحارس sentinel error، وهو خطأ مُصمّم ليكون قيمة فريدة يمكن مقارنتها دائمًا بمعنًى معين. ستحمل قيمة errUhOh السابقة دائمًا نفس المعنى (حدوث خطأ uh oh)، لذلك يمكن للبرنامج الاعتماد على مقارنة خطأ مع errUhOh لتحديد ما إذا كان هذا الخطأ قد حدث أم لا. تتضمن مكتبة جو القياسية عددًا من أخطاء الحارس لتطوير برامج جو. أحد الأمثلة على ذلك هو خطأ sql.ErrNoRows، الذي يُعاد عندما لا يُعيد استعلام قاعدة البيانات أية نتائج، لذلك يمكن معالجة هذا الخطأ بطريقة مختلفة عن خطأ الاتصال. بما أنه خطأ حارس، بالتالي يمكن استخدامه في شيفرة التحقق من الأخطاء لمعرفة متى لا يُعيد الاستعلام أية صفوف rows، ويمكن للبرنامج التعامل مع ذلك بطريقة مختلفة عن الأخطاء الأخرى.

عند إنشاء قيمة خطأ حارس، تُستخدم الدالة errors.New من حزمة errors بدلًا من دالة fmt.Errorf التي كنا نستخدمها. لا يؤدي استخدام errors.New بدلًا من fmt.Errorf إلى إجراء أي تغييرات أساسية في كيفية عمل الخطأ، ويمكن استخدام كلتا الدالتين بالتبادل في معظم الأوقات. أكبر فرق بين الاثنين هو أن errors.New تنشئ خطأ مع رسالة ثابتة، بينما تسمح دالة fmt.Errorf بتنسيق الرسالة مع القيم بطريقة مشابهة لآلية تنسيق السلاسل في fmt.Printf أو fmt.Sprintf.

نظرًا لأن أخطاء الحارس هي أخطاء أساسية بقيم لا تتغير، يُعد استخدام errors.New لإنشائها شائعًا. لنحدّث الآن البرنامج السابق من أجل استخدام خطأ الحارس مع الخطأ uh oh بدلًا من fmt.Errorf. نفتح ملف "main.go" لإضافة خطأ الحارس errUhOh الجديد وتحديث البرنامج لاستخدامه. تُحدَّث دالة validateValue بحيث تعيد خطأ الحارس بدلًا من استخدام fmt.Errorf. تُحدّث دالة main بحيث تتحقق من وجود خطأ حارس errUhOh وطباعة oh no عندما يواجهها خطأ بدلًا من رسالة :there was an error التي تظهر لأخطاء أخرى.

package main

import (
    "errors"
    "fmt"
)

var (
    errUhOh = errors.New("uh oh")
)

func validateValue(number int) error {
    if number == 1 {
        return fmt.Errorf("that's odd")
    } else if number == 2 {
        return errUhOh
    }
    return nil
}

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := validateValue(num)
        if err == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

نحفظ الشيفرة ونشغّل البرنامج كالعادة باستخدام الأمر go run:

$ go run main.go

سيُظهر الخرج هذه المرة ناتج الخطأ العام للقيمة 1، لكنه يستخدم الرسالة المخصصة !oh no عندما تصادف الخطأ errUhOh الناتج من تمرير 2 إلى validateValue:

validating 1... there was an error: that's odd
validating 2... oh no!
validating 3... valid!

يؤدي استخدام أخطاء الحارس داخل شيفرة فحص الأخطاء إلى تسهيل التعامل مع حالات الخطأ الخاصة، إذ يمكن لأخطاء الحارس مثلًا المساعدة في تحديد ما إذا كان الملف الذي نقرأه قد فشل لأننا وصلنا إلى نهاية الملف، والذي يُشار إليه بخطأ الحارس io.EOF، أو إذا فشل لسبب آخر.

أنشأنا في هذا القسم برنامج جو يستخدم خطأ حارس باستخدام errors.New للإشارة إلى حدوث نوع معين من الأخطاء. بمرور الوقت ومع نمو البرنامج، قد نصل إلى النقطة التي نريد فيها تضمين مزيدٍ من المعلومات في الخطأ الخاص بنا بدلًا من مجرد قيمة الخطأ uh oh. لا تقدم قيمة الخطأ هذه أي سياق حول مكان حدوث الخطأ أو سبب حدوثه، وقد يكون من الصعب تعقب تفاصيل الخطأ في البرامج الأكبر حجمًا. للمساعدة في استكشاف الأخطاء وإصلاحها ولتقليل وقت تصحيح الأخطاء، يمكننا الاستفادة من تغليف الأخطاء لتضمين التفاصيل التي نحتاجها.

تغليف وفك تغليف الأخطاء

يُقصد بتغليف الأخطاء أخذ قيمة خطأ معينة ووضع قيمة خطأ أخرى داخلها، وكأنها هدية مغلفة، وبما أنها مغلفة، فهذا يعني أنك بحاجة لفك غلافها لمعرفة ما تحتويه. يمكننا من خلال تغليف الخطأ تضمين معلومات إضافية حول مصدر الخطأ أو كيفية حدوثه دون فقدان قيمة الخطأ الأصلية، نظرًا لوجودها داخل الغلاف.

قبل الإصدار 1.13 كان من الممكن تغليف الأخطاء، إذ كان بالإمكان إنشاء قيم خطأ مخصصة تتضمن الخطأ الأصلي، ولكن سيتعين علينا إما إنشاء أغلفة خاصة، أو استخدام مكتبة تؤدي الغرض نيابةً عنا. بدءًا من الإصدار 1.13 أضافت لغة جو دعمًا لعملية تغليف الأخطاء وإلغاء التغليف بمثابة جزء من المكتبة القياسية عن طريق إضافة الدالة errors.Unwrap والعنصر النائب w% لدالة fmt.Errorf.

سنحدّث خلال هذا القسم برنامجنا لاستخدام العنصر النائب w% لتغليف الأخطاء بمزيد من المعلومات، وبعد ذلك ستستخدم الدالة errors.Unwrap لاسترداد المعلومات المغلفة.

تغليف الأخطاء مع الدالة fmt.Errorf

كانت الدالة fmt.Errorf تُستخدم سابقًا لإنشاء رسائل خطأ منسقة بمعلومات إضافية باستخدام عناصر نائبة مثل v% للقيم المعمّمة و s% للسلاسل النصية، أما حديثًا (بدءًا من الإصدار 1.13) أُضيف عنصر نائب جديد هو w%. عندما يجري تضمين هذا العنصر ضمن تنسيق السلسلة وتتوفر قيمة للخطأ، ستتضمّن قيمة الخطأ المُعادة من fmt.Errorf قيمة الخطأ error المُغلّف في الخطأ المُنشأ.

افتح ملف "main.go" وحدّثه ليشمل دالةً جديدةً تسمى runValidation. ستأخذ هذه الدالة الرقم الذي يجري التحقق منه حاليًا وستُشغّل أي عملية تحقق مطلوبة على هذا الرقم، وفي حالتنا ستحتاج إلى تنفيذ الدالة runValidation فقط. إذا واجه البرنامج خطأً في التحقق من القيمة، سيغلِّف الخطأ باستخدام fmt.Errorf والعنصر النائب w% لإظهار حدوث خطأ في التشغيل، ثم يعيد هذا الخطأ الجديد. ينبغي أيضًا تحديث الدالة main، فبدلًا من استدعاء validateValue مباشرةً، نستدعي runValidation:

...

var (
    errUhOh = errors.New("uh oh")
)

func runValidation(number int) error {
    err := validateValue(number)
    if err != nil {
        return fmt.Errorf("run error: %w", err)
    }
    return nil
}

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

يمكننا الآن -بعد حفظ التحديثات- تشغيل البرنامج:

$ go run main.go

سيظهر خرج مشابه لما يلي:

validating 1... there was an error: run error: that's odd
validating 2... there was an error: run error: uh oh
validating 3... valid!

هناك عدة أشياء يمكن ملاحظتها من هذا الخرج. إذ نرى أولًا رسالة الخطأ للقيمة 1 مطبوعةً الآن ومتضمنة run error: that's odd في رسالة الخطأ. يوضح هذا أن الخطأ جرى تغليفه بواسطة الدالة fmt.Errorf الخاصة بالدالة runValidation وأن قيمة الخطأ التي جرى تغليفها that's odd مُضمّنة في رسالة الخطأ. هناك مشكلة، إذ أن معالج الخطأ الخاص الذي أضفناه إلى خطأ errUhOh لم يُنفّذ، وسنرى في السطر الثاني من الخرج والذي يتحقق من صلاحية الرقم 2 -رسالة الخطأ الافتراضية للقيمة 2 وهي:

there was an error: run error: uh oh

بدلًا من الرسالة المتوقعة !oh no. نحن نعلم أن الدالة ValidateValue لا تزال تُعيد الخطأ uh oh، إذ يمكننا رؤية ذلك في نهاية الخطأ المُغلف، لكن معالج الخطأ في errUhOh لم يعد يعمل. يحدث هذا لأن الخطأ الذي أُعيد من الدالة runValidation لم يعد الخطأ errUhOh، وإنما الخطأ المغلف الذي أُنشئ بواسطة الدالة fmt.Errorf. عندما تحاول الجملة الشرطية if مقارنة متغير err مع errUhOh، فإنها تُعيد خطأ لأن errUhOh لم يعد مساويًا للخطأ الذي يُغلّفerrUhOh، ولحل هذه المشكلة يجب الحصول الخطأ من داخل الغلاف عن طريق فك التغليف باستخدام دالة errors.Unwrap.

فك تغليف الأخطاء باستخدام errors.Unwrap

إضافةً إلى العنصر النائب w% في الإصدار 1.13، أُضيفت بعض الدوال الجديدة إلى حزمة الأخطاء errors. واحدة من هذه الدوال هي الدالة errors.Unwrap، التي تأخذ خطأ error مثل معامل، وإذا كان الخطأ المُمرّر مُغلّف خطأ، ستعيد الخطأ المُغلّف، وإذا لم يكن الخطأ المُمرّر غلافًا تُعيد nil.

نفتح الآن ملف "main.go"، وباستخدام الدالة errors.Unwrap سنحدّث آلية التحقق من خطأ errUhOh لمعالجة الحالة التي يجري فيها تغليف errUhOh داخل مغلّف خطأ:

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh || errors.Unwrap(err) == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

شغّل البرنامج:

$ go run main.go

لتكون النتيجة على النحو التالي:

validating 1... there was an error: run error: that's odd
validating 2... oh no!
validating 3... valid!

سنرى الآن في السطر الثاني من الخرج أن خطأ !oh no للقيمة 2 قد ظهر. يسمح الاستدعاء الإضافي للدالة errors.Unwrap الذي أضفناه إلى تعليمة if باكتشاف errUhOh عندما تكون err هي قيمة خطأ errUhOh وكذلك إذا كان err هو خطأ يُغلّف خطأ errUhOh مباشرةً.

استخدمنا في هذا القسم العنصر w% المضاف إلى fmt.Errorf لتغليف الخطأ errUhOh داخل خطأ آخر وإعطائه معلومات إضافية. استخدمنا بعد ذلك errors.Unwrap للوصول إلى الخطأ errorUhOh المُغلّف داخل خطأ آخر. يُعد تضمين أخطاء داخل أخطاء أخرى مثل قيم ضمن سلسلة string أمرًا مقبولًا بالنسبة للأشخاص الذين يقرؤون رسائل الخطأ، ولكن قد ترغب أحيانًا في تضمين معلومات إضافية مع غلاف الأخطاء لمساعدة البرنامج في معالجة الخطأ، مثل رمز الحالة status code في خطأ طلب HTTP، وفي هكذا حالة يمكنك إنشاء خطأ مخصص جديد لإعادته. يمكنك الاطلاع على مقال كيفية استكشاف وإصلاح رموز أخطاء HTTP الشائعة على أكاديمية حسوب لمزيدٍ من المعلومات حول رموز حالة أخطاء HTTP الشائعة.

أخطاء مغلفة مخصصة

بما أن القاعدة الوحيدة للواجهة error في لغة جو هي أن تتضمن تابع Error، فمن الممكن تحويل العديد من أنواع لغة جو إلى خطأ مخصص، وتتمثل إحدى الطرق في تعريف نوع بنية struct مع معلومات إضافية حول الخطأ وتضمين تابع Error.

بالنسبة لخطأ التحقق Validation error، سيكون من المفيد معرفة القيمة التي تسببت بالخطأ. لننشئ الآن بنيةً جديدة اسمها ValueError تحتوي على حقل من أجل القيمة Value التي تسبب الخطأ وحقل Err يحتوي على الخطأ الفعلي. تستخدم عادةً أنواع الأخطاء المخصصة اللاحقة Error في نهاية اسم النوع، للإشارة إلى أنه نوع يتوافق مع الواجهة error. افتح الآن ملف "main.go" وضِف البنية الجديدة ValueError، إضافةً إلى دالة newValueError لتُنشئ نسخًا من هذه البنية.

نحتاج أيضًا إلى إنشاء تابع يُسمى Error من أجل البنية ValueError لكي تُعد من النوع error. يجب أن يُعيد التابع Error القيمة التي تريد عرضها عندما يجري تحويل الخطأ إلى سلسلة نصية. نستخدم في حالتنا الدالة fmt.Sprintf لإعادة سلسلة نصية تعرض :value error ثم الخطأ المُغلّف. حدِّث الدالة ValidateValue، فبدلًا من إعادة الخطأ الأساسي فقط، ستستخدم الدالة newValueError لإعادة خطأ مخصص:

...

var (
    errUhOh = fmt.Errorf("uh oh")
)

type ValueError struct {
    Value int
    Err   error
}

func newValueError(value int, err error) *ValueError {
    return &ValueError{
        Value: value,
        Err:   err,
    }
}

func (ve *ValueError) Error() string {
    return fmt.Sprintf("value error: %s", ve.Err)
}

...

func validateValue(number int) error {
    if number == 1 {
        return newValueError(number, fmt.Errorf("that's odd"))
    } else if number == 2 {
        return newValueError(number, errUhOh)
    }
    return nil
}

...

لنُشغّل البرنامج الآن:

$ go run main.go

ستكون النتيجة على النحو التالي:

validating 1... there was an error: run error: value error: that's odd
validating 2... there was an error: run error: value error: uh oh
validating 3... valid!

يظهر الناتج الآن أن الأخطاء مُغلّفة داخل ValueError من خلال عرض :value error قبلها، لكن نلاحظ أن خطأ uh oh لم يُكتشف مرةً أخرى لأن errUhOh داخل طبقتين من الأغلفة الآن، هما: ValueError وغلاف fmt.Errorf من runValidation. تُستخدم الدالة errors.Unwrap مرةً واحدةً فقط على الخطأ، لذلك ينتج عن استخدام (errors.Unwrap(err القيمة ValueError* وليس errUhOh. تتمثل إحدى الحلول في تعديل عملية التحقق من errUhOh بإضافة استدعاء ()errors.Unwrap مرتين لفك كلتا الطبقتين.

نفتح الآن ملف "main.go" ونُعدّل الدالة main لإضافة التعديل:

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh ||
            errors.Unwrap(err) == errUhOh ||
            errors.Unwrap(errors.Unwrap(err)) == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

لنُشغّل البرنامج الآن:

$ go run main.go

ستكون النتيجة على النحو التالي:

validating 1... there was an error: run error: value error: that's odd
validating 2... there was an error: run error: value error: uh oh
validating 3... valid!

نلاحظ أن معالجة الأخطاء الخاصة errUhOh لا تعمل، إذ كنا نتوقع ظهور خرج معالج الخطأ الخاص !oh no من أجل سطر التحقق الثاني لكن ما زالت الرسالة الافتراضية . . .:there was an error: run error تظهر بدلًا منها. هذا يحدث لأن errors.Unwrap لا تعرف كيفية فك تغليف الخطأ المخصص ValueError. يجب أن يكون لدى الخطأ المخصص تابع فك تغليف Unwrap خاص، يعيد الخطأ الداخلي مثل قيمة error.عندما كنا نُنشئ أخطاءً باستخدام fmt.Errorf مع العنصر النائب w%، كان مُصرّف جو يُنشئ خطأ مع تابع تغليف Unwrap تلقائيًا، لذلك لم نكن بحاجة إلى إضافة التابع يدويًا، لكننا هنا نستخدم دالةً خاصة، وبالتالي يجب أن نُضيف هذا التابع يدويًا.

إذًا، لإصلاح مشكلة errUhOh نفتح ملف "main.go" ونضيف التابع Unwrap إلى ValueError التي تُعيد الحقل Err الذي يحتوي على الخطأ الداخلي المغلف:

...

func (ve *ValueError) Error() string {
    return fmt.Sprintf("value error: %s", ve.Err)
}

func (ve *ValueError) Unwrap() error {
    return ve.Err
}

...

لنُشغّل البرنامج الآن:

$ go run main.go

ستكون النتيجة كما يلي:

validating 1... there was an error: run error: value error: that's odd
validating 2... oh no!
validating 3... valid!

نلاحظ أن المشكلة قد حُلت وظهرت رسالة الخطأ !oh no التي تُشير إلى الخطأ errUhOh هذه المرة، لأن errors.Unwrap أصبح قادرًا على فك تغليف ValueError.

أنشأنا في هذا القسم خطأ جديد مخصص ValueError، لتزويدنا والمستخدمين بمعلومات إضافية عن عملية التحقق في جزء من رسالة الخطأ. أضفنا أيضًا دعمًا (الدالة Unwrap) لعملية فك تغليف الأخطاء إلى ValueError، بحيث يمكن استخدام errors.Unwrap للوصول إلى الخطأ المغلف.

يمكننا أن نلاحظ عمومًا أن معالجة الأخطاء أصبحت صعبة نوعًا ما ويصعب الحفاظ عليها، إذ يجب علينا إضافة دالة فك تغليف errors.Unwrap من أجل كل طبقة تغليف جديدة. لحسن حظنا هناك دوال جاهزة errors.Is و errors.As تجعل العمل مع الأخطاء المغلفة أسهل.

التعامل مع الأخطاء المغلفة Wrapped Errors

عند الحاجة إلى إضافة استدعاء جديد للدالة errors.Unwrap من أجل كل طبقة تغليف في البرنامج، سيستغرق الأمر وقتًا طويلًا ويصعب الحفاظ عليه. لهذا الأمر أُضيفت الدالتان errors.Is و errors.As إلى حزمة الأخطاء errors بدءًا من الإصدار 1.13 من لغة جو، إذ تعمل هاتان الدالتان على تسهيل التعامل مع الأخطاء من خلال السماح لك بالتفاعل مع الأخطاء بغض النظر عن مدى عمق تغليفها داخل الأخطاء الأخرى. تسمح الدالة errors.Is بالتحقق ما إذا كانت قيمة خطأ حارس معين موجودةً في أي مكان داخل خطأ مغلف؛ بينما تتيح الدالة errors.As الحصول على مرجع لنوع معين من الأخطاء من أي مكان داخل خطأ مغلّف.

فحص قيمة خطأ باستخدام الدالة errors.Is

يؤدي استخدام الدالة errors.Is للتحقق من وجود خطأ معين إلى جعل معالجة الخطأ الخاص errUhOh أقصر بكثير، لأنه يعالج جميع الأخطاء المتداخلة تلقائيًّا بدل إجرائها يدويًا من قبلنا. تأخذ الدالة معاملين كل منهما خطأ error، المعامل الأول هو الخطأ الذي تلقيناه والثاني هو الخطأ الذي نريد التحقق منه. لإزالة التعقيدات من عملية معالجة الخطأ errUhOh، سنفتح ملف "main.go"، ونحدّث عملية التحقق من errUhOh في دالة main لنستخدم الدالة errors.Is:

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if errors.Is(err, errUhOh) {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

لنُشغّل البرنامج الآن:

$ go run main.go

سنحصل على النتيجة التالية:

validating 1... there was an error: run error: value error: that's odd
validating 2... oh no!
validating 3... valid!

يظهر الخرج رسالة الخطأ !oh no، وهذا يعني أنه على الرغم من وجود عملية فحص خطأ واحدة من أجل errUhOh، سيظل بالإمكان فك سلسلة الأخطاء والوصول إلى الخطأ المُغلّف. تستفيد الدالة errors.Is من التابع Unwrap لمواصلة البحث العميق في سلسلة من الأخطاء، حتى تعثر على قيمة الخطأ المطلوبة أو خطأ حارس أو تُصادف تابع Unwrap يعيد قيمة nil. بعد إضافة دالة errors.Is في إصدار 1.13، أصبح استخدامها موصى به للتحقق من وجود أخطاء. تجدر الإشارة إلى أنه يمكن استخدام هذه الدالة مع قيم الخطأ الأخرى، مثل خطأ sql.ErrNoRows سالف الذكر.

استرداد نوع الخطأ باستخدام errors.As

الدالة الثانية التي سنتحدث عنها هي errors.As، والتي تُستخدم عندما نريد الحصول على مرجع لنوع معين من الأخطاء للتفاعل معه بتفصيل أكبر. مثلًا يتيح الخطأ المخصص ValueError الذي أضفناه سابقًا؛ الوصول إلى القيمة الفعلية التي يجري التحقق من صحتها في حقل Value الخاص بالخطأ، ولكن لا يمكن الوصول إليه إلا إذا كان لدينا مرجع لهذا الخطأ أولًا. هنا يأتي دور errors.As التي تأخذ معاملين، الأول خطأ والثانية متغير لنوع الخطأ، إذ يجري المرور على سلسلة الأخطاء لمعرفة ما إذا كان أي من الأخطاء المغلفة يتطابق مع النوع المُقدم، فإذا حصل تطابق مع أحدها فسيُضبط المتغير المُمرر لنوع الخطأ بالخطأ الذي عثر عليه errors.As، وستعيد الدالة true وإلا ستعيد false.

إذًا، أصبح بإمكاننا باستخدام هذه الدلة الاستفادة من نوع ValueError لإظهار معلومات إضافية عن الخطأ في معالج الأخطاء. لنفتح ملف "main.go" للمرة الأخيرة ونحدّث الدالة main لإضافة حالة جديدة لمعالجة الأخطاء من نوع ValueError، بحيث تطبع قيمة الخطأ والرقم غير الصالح وخطأ التحقق:

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)

        var valueErr *ValueError
        if errors.Is(err, errUhOh) {
            fmt.Println("oh no!")
        } else if errors.As(err, &valueErr) {
            fmt.Printf("value error (%d): %v\n", valueErr.Value, valueErr.Err)
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

صرّحنا في الشيفرة السابقة عن متغير جديد اسمه valueErr واستخدمنا errors.As، للحصول على مرجع إلى ValueError في حال كان مغلفًا داخل قيمة err. بمجرد الوصول إلى الخطأ الخاص ValueError، سنتمكن من الوصول إلى أية حقول إضافية يوفرها هذا النوع، مثل القيمة الفعلية التي فشل التحقق منها. قد يكون هذا مفيدًا إذا كانت عملية التحقق عميقة في البرنامج ولم يكن لدينا إمكانية الوصول إلى القيم لمنح المستخدمين تلميحات حول مكان حدوث خطأ ما. مثال آخر على المكان الذي يمكن أن يكون مفيدًا فيه هو في حال كنا في صدد برمجة شبكة وواجهنا خطأ net.DNSError. يمكنك -من خلال الحصول على مرجع للخطأ- معرفة ما إذا كان الخطأ ناتجًا عن عدم القدرة على الاتصال، أو ما إذا كان الخطأ ناتجًا عن القدرة على الاتصال، ولكن لم يُعثر على المورد المطلوب، وهذا يمكّننا من التعامل مع الخطأ بطرق مختلفة. لرؤية كيف تجري الأمور مع الدالة errors.As دعونا نُشغّل البرنامج:

$ go run main.go

ستكون النتيجة على النحو التالي:

validating 1... value error (1): that's odd
validating 2... oh no!
validating 3... valid!

لن ترى رسالة الخطأ الافتراضي … :there was an error هذه المرة في الخرج، لأن جميع الأخطاء تُعالج بواسطة معالجات الأخطاء الأخرى. يظهر السطر اﻷول من الخرج الخاص بالتحقق من صحة القيمة 1 أن الدالة errors.As تُعيد true لأن رسالة الخطأ ... value error عُرضت. بما أن الدالة errors.As تُعيد true، يُضبط المتغير valueErr ليكون خطأ ValueError ويمكن استخدامه لطباعة القيمة التي فشلت في التحقق من الصحة من خلال الوصول إلى valueErr.Value (لاحظ كيف طُبعت القيمة 1 والتي فشلت في اختبار التحقق من الصحة). يظهر أيضًا السطر الثاني من الخرج، والذي يشير إلى اختبار التحقق من صحة الرقم 2، أنه على الرغم من تغليف errUhOh داخل غلاف ValueError، ما زالت رسالة الخطأ !oh no تظهر (بالرغم من وجود طبقتي تغليف)، وهذا لأن معالج الأخطاء الخاص الذي يستخدم الدالة errors.Is مع errUhOh تأتي أولًا في مجموعة تعليمات اختبار if لمعالجة الأخطاء. بما أن هذا المعالج يُعيد true قبل تنفيذ errors.As، يُنفّذ معالج !oh no. لو كانت الدالة errors.As تظهر قبل الدالة errors.Is في البرنامج لرأينا خرجًا مشابهًا لحالة القيمة 1، أي أن رسالة !oh no ستكون value error (2): uh oh.

حدّثنا خلال هذا القسم البرنامج لنتمكن من استخدام errors.Is لإزالة الاستدعاءات الكثيرة والإضافية للدالة errors.Unwrap وجعل شيفرة معالجة الأخطاء أكثر متانة وسهولة للتعديل. استخدمنا أيضًا الدالة errors.As للتحقق ما إذا كان أي من الأخطاء المغلّفة هو ValueError، ثم استخدمنا حقولها في حال توافرها.

الخاتمة

تعرّفنا خلال هذا المقال على كيفية تغليف خطأ باستخدام العنصر النائب w% وكيفية فكه باستخدام الدالة errors.Unwrap. أنشأنا أيضًا نوع خطأ مخصص يدعم الدالة errors.Unwrap، واستخدمناه لاستكشاف دوال مُساعدة جديدة errors.Is و errors.As تُسهّل علينا عملية إرفاق معلومات أعمق وأكثر تفصيلًا عن الأخطاء التي نُنشئها، أو نتعامل معها وتضمن استمرارية عملية فحص الأخطاء حتى في حالة الأخطاء العميقة.

ترجمة -وبتصرف- للمقال How to Add Extra Information to Errors in Go لصاحبه Kristin Davidson.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...