تحرير معالجة الأخطاء عن طريق التخلص من الأخطاء



يهدف Go2 إلى تقليل الحمل العام لمعالجة الأخطاء ، ولكن هل تعلم ما هو أفضل من بناء الجملة المحسّن لمعالجة الأخطاء؟

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

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

المثال الأول


فيما يلي الدالة لحساب عدد الأسطر في ملف:

func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil } 

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

على سبيل المثال ، هناك مثل هذا البناء الغريب:

 _, err = br.ReadString('\n') lines++ if err != nil { break } 

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

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

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

 return lines, err 

أعتقد أن هذا مثال جيد لملاحظة روس كوكس بأن معالجة الأخطاء يمكن أن تجعل المهمة أكثر صعوبة . لنلقِ نظرة على النسخة المحسّنة.

 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() } 

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

ترجع طريقة sc.Scan () صوابًا إذا عثر الماسح الضوئي على سطر من النص ولم يعثر على خطأ. وبالتالي ، لن يتم استدعاء نص حلقة for for إلا عند وجود سطر من النص في المخزن المؤقت للماسحة الضوئية. هذا يعني أن لدينا إعادة قراءة CountLines يعالج الحالة بشكل صحيح عندما لا يكون هناك حرف السطر زائدة. الآن أيضًا الحالة عندما يكون الملف فارغًا بشكل صحيح.

ثانياً ، نظرًا لأن sc.Scan تُرجع خطأ عند حدوث خطأ ، فإن حلقة التكرار الخاصة بنا ستنتهي عند الوصول إلى نهاية الملف أو حدوث خطأ. اكتب bufio.Scanner يتذكر الخطأ الأول الذي تم اكتشافه ، ونصلح هذا الخطأ بعد الخروج من الحلقة باستخدام طريقة sc.Err ().

أخيرًا ، يتولى buffo.Scanner معالجة io.EOF وتحويله إلى الصفر في حالة الوصول إلى نهاية الملف دون خطأ.

المثال الثاني


المثال الثاني الخاص بي مستوحى من أخطاء Rob Pikes ، وهي قيم تدوينة في مدونة.

عند العمل على فتح الملفات وكتابتها وإغلاقها ، تعتبر معالجة الأخطاء أمرًا مثيرًا للإعجاب ، ولكن يمكن الانتهاء من العمليات في مساعدين مثل ioutil.ReadFile و ioutil.WriteFile. ومع ذلك ، عند العمل مع بروتوكولات الشبكة ذات المستوى المنخفض ، غالبًا ما يكون من الضروري إنشاء استجابة مباشرة باستخدام بدائل I / O ، لذلك قد تبدأ معالجة الأخطاء في التكرار. ضع في اعتبارك هذا الجزء من خادم HTTP الذي ينشئ استجابة HTTP / 1.1:

 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } 

أولاً ، ننشئ شريط الحالة باستخدام fmt.Fprintf ونتحقق من وجود خطأ. ثم ، لكل رأس ، نقوم بتسجيل مفتاح وقيمة الرأس ، في كل مرة نتحقق من وجود خطأ. أخيرًا ، ننهي قسم الرأس بـ \ r \ n إضافي ، ونفحص الخطأ ، وننسخ نص الاستجابة إلى العميل. أخيرًا ، رغم أننا لسنا بحاجة إلى التحقق من الخطأ من io.Copy ، إلا أننا نحتاج إلى تحويله من نموذج له قيمتان للإرجاع ، والتي ترجع io.Copy إلى قيمة الإرجاع الفردية التي تتوقعها WriteResponse.

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

 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } 

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

 func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err } 

تطبيق errWriter على WriteResponse يحسن وضوح التعليمات البرمجية. كل عملية لم تعد بحاجة إلى قصر نفسها على التحقق من الأخطاء. تنتقل رسالة الخطأ إلى نهاية الوظيفة ، وتتحقق من حقل ew.err وتجنب الترجمة المزعجة لقيم io.Copy التي يتم إرجاعها

الخاتمة


عندما تواجه معالجة خطأ مفرطة ، حاول استخراج بعض العمليات كنوع برنامج تجميع إضافي.

عن المؤلف


مؤلف هذا المقال ، ديف تشيني ، مؤلف العديد من الحزم الشائعة لـ Go ، مثل github.com/pkg/errors و github.com/davecheney/httpstat .

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


All Articles