عندما تفشل دالة في لغة جو، فإنها تُعيد قيمةً باستخدام الواجهة error
للسماح للمُستدعي بمعالجة الخطأ. في كثير من الأحيان يستخدم المطورون الدالة fmt.Errorf
من الحزمة fmt
لإعادة هذه القيم. قبل الإصدار 1.13 من لغة جو، كان الجانب السلبي لاستخدام هذه الدالة هو أننا سنفقد المعلومات المتعلقة بالأخطاء. لحل هذه المشكلة استخدم المطورون حزمًا توفر أساليب لتغليف wrap هذه الأخطاء داخل أخطاء أخرى، أو إنشاء أخطاء مخصصة من خلال تنفيذ التابع Error() string
على أحد أنواع خطأ struct
الخاصة بهم. أحيانًا، يكون إنشاء هذه الأنواع من struct
مملًا إذا كان لديك عدد من الأخطاء التي لا تحتاج إلى المعالجة الصريحة من قبل من المُستدعي.
جاء إصدار جو 1.13 مع ميزات لتسهيل التعامل مع هذه الحالات، وتتمثل إحدى الميزات في القدرة على تغليف الأخطاء باستخدام دالة fmt.Errorf
بقيمة خطأ error
يمكن فكها لاحقًا للوصول إلى الأخطاء المغلفة. بالتالي، أنهى تضمين دالة تغليف للأخطاء داخل مكتبة جو القياسية الحاجة إلى استخدام طرق ومكتبات خارجية. إضافةً إلى ذلك، تجعل الدالتان errors.Is
و errors.As
من السهل تحديد ما إذا كان خطأ محدد مُغلّف في أي مكان داخل خطأ ما، ويمنحنا الوصول إلى هذا الخطأ مباشرةً دون الحاجة إلى فك جميع الأخطاء يدويًا.
سننشئ في هذا المقال برنامجًا يستخدم هذه الدوال لإرفاق معلومات إضافية مع الأخطاء التي تُعاد من الدوال، ثم سننشئ بنية struct
للأخطاء المخصصة والتي تدعم دوال التغليف وفك التغليف.
المتطلبات
لتتابع هذا المقال، يجب أن تستوفي الشروط التالية:
- إصدار مُثبّت من جو 1.13 أو أعلى، ويمكنك الاستعانة بمقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu لإعداده.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز.
- (اختياري) قراءة مقال معالجة الأخطاء في لغة جو Go ، قد يكون مفيدًا للحصول على شرح أكثر تعمقًا لمعالجة الأخطاء. عمومًا نحن نغطي بعض الموضوعات منها في هذا المقال، لكن بشرح عالي المستوى.
- (اختياري) هذ المقال نوعًا ما هو امتداد لمقال معالجة حالات الانهيار في لغة جو Go مع الإشارة إلى بعض الميزات. قراءة المقالة السابقة مهم أو مفيد، لكن ليس ضروري.
إعادة ومعالجة الأخطاء في لغة جو
تُعد معالجة الأخطاء من الممارسات الجيدة التي تحدث أثناء تنفيذ البرامج حتى لا يراها المستخدمون أبدًا، لكن لا بُد من التعرّف عليها أولًا لمعالجتها. يمكننا في لغة جو معالجة الأخطاء في البرامج من خلال إعادة معلومات متعلقة بهذه الأخطاء من الدوال التي حدث فيها الخطأ باستخدام واجهة 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.