ثلاثة أنواع من الأخطاء
تستخدم شيفرات Go ثلاثة أشكال من الأخطاء، من الأبسط إلى الأغنى:
- خطأ عابر:
errors.New("...")أوfmt.Errorf("...")يُنشأ في مكانه. لا يستطيع المستدعون إلا قراءة الرسالة. - خطأ حارس (sentinel): متغيّر على مستوى الحزمة مثل
io.EOF. يستطيع المستدعون فحصه بـerrors.Is. - نوع خطأ: بنية لها تابع
Error()وتحمل حقولًا. يستطيع المستدعون استخراجها بـerrors.Asوقراءة الحقول.
اختر الأبسط الذي يسمح للمستدعين بفعل ما يحتاجونه.
الأخطاء الحارسة
الخطأ الحارس قيمة خطأ تُعرَّف مرة واحدة وتُقارن بهويتها. تبدأ أسماؤها بـ Err اصطلاحًا.
المخرجات:
mug bought
cap is out of stock, notify me later
buy "hat": not found
استدعاءان لـ errors.New بالنص نفسه ينتجان خطأين مختلفين: errors.New("x") == errors.New("x") تساوي false. لهذا يجب أن يكون الخطأ الحارس متغيّرًا مشتركًا واحدًا.
تصبح الأخطاء الحارسة جزءًا من الواجهة البرمجية لحزمتك. بمجرّد أن يفحص المستدعون ErrNotFound لا تستطيع التوقّف عن إعادته دون كسر شيفرتهم. صدّر فقط ما يحتاج المستدعون فعلًا إلى التفرّع عليه.
أنواع الأخطاء المخصّصة
عندما يحتاج المستدعي إلى تفاصيل (أي حقل، أي رمز حالة، أي مهلة لإعادة المحاولة)، عرّف نوعًا.
المخرجات:
register: age: must be between 0 and 150
bad field: age
للتابع مستقبِل مؤشر والدالة تعيد &ValidationError{...}، لذا فهدف errors.As هو *ValidationError، وتمرّر عنوانه (&ve، وهو **ValidationError). الخطأ في هذا المستوى هو الخطأ الكلاسيكي مع errors.As: مع مستقبِل قيمة كنت ستعيد ValidationError{...} وتعرّف var ve ValidationError. يكتشف go vet الزلّة الأشيع، أي تمرير ve بدل &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.
التغليف بـ %w
يعيد fmt.Errorf مع %w خطأ يتذكّر الخطأ الذي يغلّفه. كل طبقة تضيف سياقًا وتحافظ على السلسلة سليمة.
المخرجات:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
مع %v تبقى الرسالة نفسها لكن السلسلة تنقطع، فتعيد errors.Is القيمة false. اختر %w عندما يجب أن يرى المستدعون السبب، و%v عندما يكون السبب تفصيلًا داخليًا لا تريدهم أن يعتمدوا عليه.
منذ Go 1.20 يمكن لاستدعاء واحد تغليف عدة أخطاء: fmt.Errorf("%w; %w", err1, err2). وعندها تطابق errors.Is أيًا منهما.
errors.Join: عدة أخطاء دفعة واحدة
التحقّق من المدخلات والتنظيف ينتجان غالبًا أكثر من خطأ. تجمعها errors.Join (Go 1.20).
المخرجات:
name: required
email: required
age: negative
---
true
true
تعيد errors.Join القيمة nil عندما تكون كل الوسائط nil، فلا تحتاج الدالة السابقة حالة خاصة لغياب الأخطاء.
التابعان Unwrap وIs
تجد errors.Is وerrors.As الأخطاء المغلّفة باستدعاء تابع Unwrap. النوع المخصّص الذي يحمل سببًا يجب أن يكشفه:
النوع الذي يغلّف عدة أخطاء يطبّق Unwrap() []error بدلًا من ذلك.
ويمكن للنوع أيضًا تعريف Is(target error) bool ليقرّر المساواة بنفسه، مثلًا لمطابقة أي *HTTPError له رمز الحالة نفسه. نادرًا ما تحتاجه؛ تغليف خطأ حارس وإعادته من Unwrap يكفي عادة.
فخ nil ذي النوع
لا تعرّف متغيّر خطأ بنوعك المحدّد ثم تعيده عبر error:
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
يكون if err != nil عند المستدعي صحيحًا، لأن الواجهة التي تحمل مؤشرًا nil ذا نوع ليست nil. أعد nil حرفيًا عند النجاح، واجعل متغيّرات الأخطاء المحلية من النوع error. تشرح صفحة الواجهات السبب.
أي نوع تختار
| ما يحتاجه المستدعون | ما توفّره |
|---|---|
| تسجيل الفشل أو عرضه فقط | fmt.Errorf("...: %w", err) |
| التفرّع على حالة محدّدة واحدة | خطأ حارس var ErrX = errors.New(...) |
| قراءة تفاصيل عن الفشل | نوع خطأ بحقول |
| رؤية عدة إخفاقات مستقلّة | errors.Join |
أخطاء شائعة
- مقارنة الأخطاء المغلّفة بـ
==. استخدمerrors.Is. - تمرير قيمة ليست مؤشرًا إلى
errors.As. تحتاج مؤشرًا إلى متغيّر من النوع الهدف. - إنشاء "خطأ حارس" داخل دالة.
return errors.New("not found")تنشئ قيمة جديدة في كل استدعاء؛ لا يستطيع المستدعون المقارنة بها. - تصدير كل خطأ. كل خطأ حارس أو نوع مُصدَّر وعدٌ في الواجهة البرمجية.
- المطابقة على
err.Error(). النصوص للبشر.
الأسئلة الشائعة
كيف أنشئ خطأ مخصّصًا في Go؟
لحالة ثابتة، عرّف خطأ حارسًا على مستوى الحزمة: var ErrNotFound = errors.New("not found"). ولخطأ يحمل بيانات، عرّف نوعًا له تابع Error() string: type ValidationError struct { Field string } وfunc (e *ValidationError) Error() string { return e.Field + " is invalid" }.
كيف تغلّف خطأ في Go؟
استخدم fmt.Errorf مع الفعل %w: return fmt.Errorf("load user %d: %w", id, err). تتضمّن رسالة الخطأ الجديد رسالة القديم، وتستطيع errors.Unwrap وerrors.Is وerrors.As الوصول إلى الأصل. منذ Go 1.20 يمكن لاستدعاء Errorf واحد تغليف عدة أخطاء بعدة أفعال %w.
ما الفرق بين errors.Is وerrors.As؟
تجيب errors.Is(err, target) عن سؤال "هل هذه القيمة بعينها موجودة في أي مكان من السلسلة"، للأخطاء الحارسة مثل io.EOF. وتجيب errors.As(err, &target) عن سؤال "هل في السلسلة خطأ من هذا النوع"، وإن وُجد تخزّنه في target لتقرأ حقوله.
ماذا تفعل errors.Join؟
تدمج errors.Join(errs...) (Go 1.20) عدة أخطاء في خطأ واحد. رسالتها هي الرسائل المفردة مفصولة بأسطر جديدة، وتُحذف الأخطاء nil، وتعيد nil إذا كانت كلها nil. وتطابق errors.Is وerrors.As إذا طابق أي من الأخطاء المدمجة.