التعامل مع الأخطاء في Go 1.13


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

 type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() } 

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

النمط ، عندما يحتوي أحد الأخطاء على خطأ آخر ، يتم مواجهته في كثير من الأحيان في Go ، وبعد نقاش ساخن في Go 1.13 تمت إضافة دعمه الصريح. في هذه المقالة ، سننظر في الإضافات إلى المكتبة القياسية التي توفر الدعم المذكور: ثلاث وظائف جديدة في حزمة الأخطاء وأمر تنسيق جديد لـ fmt.Errorf .

قبل مناقشة التغييرات بالتفصيل ، دعنا نتحدث عن كيفية التحقيق في الأخطاء وصنعها في الإصدارات السابقة من اللغة.

الأخطاء قبل الذهاب 1.13


خطأ البحث


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

 if err != nil { // something went wrong } 

في بعض الأحيان نقوم بمقارنة الخطأ لمعرفة قيمة التحكم ومعرفة ما إذا كان قد حدث خطأ معين.

 var ErrNotFound = errors.New("not found") if err == ErrNotFound { // something wasn't found } 

يمكن أن تكون قيمة الخطأ من أي نوع تفي بواجهة الخطأ المحددة في اللغة. يمكن للبرنامج استخدام عبارة كتابة أو تبديل نوع لعرض قيمة الخطأ لنوع أكثر تحديدًا.

 type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 

إضافة معلومات


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

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

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

 type QueryError struct { Query string Err error } 

يمكن للبرامج البحث داخل *QueryError واتخاذ قرار بناءً على الخطأ الأصلي. يسمى هذا أحيانًا بفك الخطأ.

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

يعد نوع os.PathError من المكتبة القياسية مثالًا آخر على كيفية os.PathError أحد الأخطاء على خطأ آخر.

الأخطاء في الذهاب 1.13


طريقة التفاف


في Go 1.13 ، تبسط الأخطاء وحزم مكتبة fmt القياسية fmt الأخطاء التي تحتوي على أخطاء أخرى. الأهم من ذلك هو الاصطلاح ، وليس التغيير: يمكن أن يحتوي الخطأ الذي يحتوي على خطأ آخر على طريقة Unwrap ، والتي تُرجع الخطأ الأصلي. إذا قامت e1.Unwrap() بإرجاع e2 ، فإننا نقول أن e1 حزم e2 ويمكنك فك e1 للحصول على e2 .

وفقًا لهذه الاتفاقية ، يمكنك إعطاء نوع QueryError الموصوف أعلاه إلى طريقة QueryError ، والتي تُرجع الخطأ الموجود فيه:

 func (e *QueryError) Unwrap() error { return e.Err } 

قد تحتوي نتيجة تفريغ الخطأ أيضًا على طريقة Unwrap . تسلسل الأخطاء التي تم الحصول عليها من خلال التفريغ المتكرر ، نسميها سلسلة من الأخطاء .

خطأ التحقيق مع هل وكما


في Go 1.13 ، تحتوي حزمة errors على وظيفتين جديدتين للتحقق من الأخطاء: Is و As .

يقارن errors.Is دالة خطأ مع قيمة.

 // Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } 

تقوم الدالة As بالتحقق مما إذا كان الخطأ من نوع معين.

 // Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

في أبسط الحالات ، تتصرف errors.Is تعمل مثل المقارنة مع خطأ التحكم ، وتتصرف errors.As كما تتصرف مثل بيان النوع. ومع ذلك ، عند التعامل مع الأخطاء المحزومة ، تقوم هذه الوظائف بتقييم جميع الأخطاء في السلسلة. دعونا نلقي نظرة على مثال QueryError أعلاه لفحص الخطأ الأصلي:

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

باستخدام errors.Is يعمل ، errors.Is كتابة هذا:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

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

خطأ التعبئة والتغليف مع ٪ ث


كما ذكرت ، من الممارسات المعتادة استخدام دالة fmt.Errorf لإضافة معلومات إضافية إلى الخطأ.

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

في Go 1.13 ، تدعم وظيفة fmt.Errorf الأمر %w الجديد. إذا كان الأمر كذلك ، fmt.Errorf الخطأ الذي تم إرجاعه بواسطة fmt.Errorf على طريقة fmt.Errorf التي تُرجع الوسيطة %w ، والتي يجب أن تكون خطأ. في جميع الحالات الأخرى ، %w مطابق لـ %v .

 if err != nil { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

حزم الخطأ مع %w يجعله متاحًا errors.As

 err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ... 

متى لحزم؟


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

على سبيل المثال ، تخيل دالة Parse تقوم بقراءة بنية بيانات معقدة من io.Reader . في حالة حدوث خطأ ، سوف نرغب في معرفة عدد الصف والعمود الذي حدث فيه. إذا حدث خطأ أثناء القراءة من io.Reader ، io.Reader إلى حزمه لمعرفة السبب. نظرًا io.Reader بوظيفة io.Reader ، فمن المنطقي إظهار الخطأ الذي تم إنشاؤه.

حالة أخرى: ربما لا تقوم دالة تقوم بإجراء عدة استدعاءات لقواعد البيانات بإرجاع خطأ يتم فيه تعبئة إحدى هذه الاستدعاءات. إذا كانت قاعدة البيانات المستخدمة من قبل هذه الوظيفة جزءًا من التنفيذ ، فإن الكشف عن هذه الأخطاء ينتهك التجريد. على سبيل المثال ، إذا كانت الدالة LookupUser من حزمة pkg تستخدم حزمة Go database/sql ، فقد تواجه خطأ sql.ErrNoRows . إذا قمت بإرجاع خطأ باستخدام fmt.Errorf("accessing DB: %v", err) ، فلن يتمكن المتصل من البحث في الداخل والعثور على sql.ErrNoRows . ولكن إذا fmt.Errorf("accessing DB: %w", err) الدالة fmt.Errorf("accessing DB: %w", err) ، فيمكن للمتصل الكتابة:

 err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) … 

في هذه الحالة ، يجب أن تقوم الدالة دائمًا بإرجاع sql.ErrNoRows إذا كنت لا تريد sql.ErrNoRows العملاء ، حتى عند التبديل إلى حزمة باستخدام قاعدة بيانات مختلفة. وبعبارة أخرى ، فإن التغليف يجعل جزء الخطأ من واجهة برمجة التطبيقات. إذا كنت لا ترغب في الالتزام بدعم هذا الخطأ في المستقبل كجزء من واجهة برمجة التطبيقات ، فلا تقم بتثبيته.

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

إعداد اختبار خطأ باستخدام هل وطرق


errors.Is وظيفة يتحقق كل خطأ في السلسلة مقابل القيمة الهدف. بشكل افتراضي ، خطأ يطابق هذه القيمة إذا كانت مكافئة. بالإضافة إلى ذلك ، يمكن أن يعلن خطأ في السلسلة عن امتثاله للقيمة المستهدفة باستخدام تنفيذ الأسلوب Is .

ضع في اعتبارك الخطأ الذي تسببه حزمة Upspin ، والتي تقارن الخطأ بالقالب وتقيم الحقول غير الصفرية فقط:

 type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) { // err's User field is "someuser". } 

تنصح errors.As الدالة As ، إن وجدت.

الأخطاء وواجهات برمجة التطبيقات للحزم


يجب أن تصف الحزمة التي تُرجع الأخطاء (ومعظم الحزم تفعل ذلك) خصائص هذه الأخطاء التي يمكن للمبرمج الاعتماد عليها. سوف تتجنب الحزمة المصممة جيدًا أيضًا إرجاع الأخطاء مع الخصائص التي لا يمكن الاعتماد عليها.

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

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

 var ErrNotFound = errors.New("not found") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

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

في أي حال ، لا تكشف عن التفاصيل الداخلية للمستخدم. كما هو مذكور في الفصل "متى يستحق التعبئة؟" ، إذا قمت بإرجاع خطأ من حزمة أخرى ، فقم بتحويلها حتى لا تكشف عن الخطأ الأصلي ، إلا إذا كنت تنوي أن تلزم نفسك بإرجاع هذا الخطأ المحدد في المستقبل.

 f, err := os.Open(filename) if err != nil { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

في حالة إرجاع دالة لخطأ ذي قيمة إشارة مكتوبة أو كتابة ، فلا تُرجع الخطأ الأصلي مباشرةً.

 var ErrPermission = errors.New("permission denied") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

استنتاج


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

كما قال روس كوكس في خطابه في GopherCon 2019 ، في الطريق إلى Go 2 ، نقوم بالتجربة والتبسيط والشحن . والآن ، وبعد شحن هذه التغييرات ، بدأنا في إجراء تجارب جديدة.

Source: https://habr.com/ru/post/ar473658/


All Articles