ليس بدون ذعر في Go

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


خطأ


الخطأ هو واجهة. ومثل معظم الواجهات في Go ، فإن تعريف الخطأ قصير وبسيط:


type error interface { Error() string } 

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


هناك وظيفتان في مكتبة Go القياسية يتم استخدامها بشكل ملائم لإنشاء الأخطاء. الأخطاء. الوظيفة الجديدة مناسبة تمامًا لإنشاء أخطاء بسيطة. تتيح وظيفة fmt.Errorf استخدام التنسيق القياسي.


 err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id) 

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


 // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } 

ستحتوي قيمة مثل هذا الخطأ على العملية والمسار والخطأ.


تتم تهيئتها بهذه الطريقة:


 ... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e} 

يمكن أن يكون للمعالجة شكل قياسي:


 _, err := os.Open("---") if err != nil{ fmt.Println(err) } // open ---: The system cannot find the file specified. 

ولكن إذا كانت هناك حاجة للحصول على معلومات إضافية ، فيمكنك فك الخطأ في * os.PathError :


 _, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) } // Err: The system cannot find the file specified. // Op: open // Path: --- 

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


الإعلان عن عدة أنواع من الأخطاء ، لكل منها بياناته الخاصة:


كود
 type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() } 

دالة يمكنها إرجاع هذه الأخطاء:


كود
 func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil } 

معالجة الخطأ من خلال نوع القوالب:


كود
 func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } } 

في حالة عدم حاجة الأخطاء إلى خصائص خاصة ، من الممارسة السليمة في Go إنشاء متغيرات لتخزين الأخطاء على مستوى الحزمة. مثال على ذلك أخطاء مثل io.EOF و io.ErrNoProgress ، وما إلى ذلك.


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


 func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } } 

يعد هذا فعالًا لأنه يتم إنشاء الأخطاء مرة واحدة فقط وإعادة استخدامها.


تتبع المكدس


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


غالبًا ما تفتقر Go إلى هذه المعلومات عن طريق الخطأ ، ولكن لحسن الحظ الحصول على تفريغ في Go ليس صعبًا.


يمكنك استخدام debug.PrintStack () لإخراج التتبع إلى الإخراج القياسي:


 func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() } 

نتيجة لذلك ، سيتم كتابة المعلومات التالية إلى Stderr:


كومة
 goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27 

يعرض debug.Stack () شريحة بايت مع تفريغ مكدس ، والتي يمكن تسجيلها لاحقًا أو في أي مكان آخر.


 b := debug.Stack() fmt.Printf("Trace:\n %s\n", b) 

هناك نقطة أخرى إذا فعلنا ذلك:


 go bar() 

ثم نحصل على المعلومات التالية عند الإخراج:


 main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c 

كل غوروتين له كومة منفصلة ، على التوالي ، نحصل فقط على تفريغها. بالمناسبة ، لدى الغوروتينات مداخنهم الخاصة ، ولا يزال الاسترداد مرتبطًا بهذا ، ولكن المزيد عن ذلك لاحقًا.
وهكذا ، لمعرفة معلومات عن جميع goroutines ، يمكنك استخدام runtime.Stack () وتمرير الوسيطة الثانية true.


 func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) } 

كومة
 Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27 

أضف هذه المعلومات إلى الخطأ وبالتالي زيادة محتوى المعلومات بشكل كبير.
على سبيل المثال ، مثل هذا:


 type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() } 

يمكنك إضافة وظيفة لإنشاء هذا الخطأ:


 func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} } 

ثم يمكنك العمل بالفعل مع هذا:


 func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err } 

كومة
 Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29 

وفقًا لذلك ، يمكن تقسيم الخطأ والتتبع:


 func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } } 

وبالطبع يوجد بالفعل حل جاهز. أحدها حزمة https://github.com/pkg/errors . يسمح لك بإنشاء خطأ جديد ، والذي سيحتوي بالفعل على مكدس التتبع ، ويمكنك إضافة تتبع و / أو رسالة إضافية إلى خطأ موجود. بالإضافة إلى تنسيق الإخراج المريح.


 import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") } 

كومة
 error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 

سيعرض٪ v الرسائل فقط


 error2: error 

الذعر / التعافي


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


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


يمكنك تمرير أي وسيطة إلى استدعاء دالة الذعر.


 panic(v interface{}) 

من المثير للذعر تمرير خطأ من النوع الذي يبسط الاسترداد ويساعد على تصحيح الأخطاء.


 panic(errors.New("error")) 

يعتمد التعافي من الكوارث في Go على استدعاء دالة مؤجلة ، يُعرف أيضًا بالتأجيل . ويضمن تنفيذ هذه الوظيفة عند العودة من الوظيفة الأم. بغض النظر عن السبب - بيان العودة أو نهاية الوظيفة أو الذعر.


والآن تتيح وظيفة الاسترداد الحصول على معلومات حول الحادث وإيقاف فك مكدس المكالمة.
مكالمة ومعالج ذعر نموذجي:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) } 

استرداد الواجهة الواجهة {} (التي نمررها للذعر) أو لا شيء إذا لم يكن هناك دعوة للذعر.


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


 func bar(f *os.File) { panic(errors.New("error")) } 

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


ثانيًا ، يجب ألا يؤدي التنفيذ غير الصحيح لهذه الوظيفة إلى نهاية البرنامج بالكامل.


يمكن حل هذه المشكلة بتأجيل واسترداد وإغلاق:


 func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error) //   ,   ,     // err := errors.New("trapped panic: %s (%T)", r, r) //     } file.Close() //   }() bar(file) return err } 

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


هناك حالات عكسية عندما يجب أن تعمل وظيفة بحجج معينة بشكل صحيح دائمًا ، وإذا لم يحدث ذلك ، فستكون سيئة حقًا.


في مثل هذه الحالات ، أضف دالة مجمعة يتم فيها استدعاء الوظيفة المستهدفة ، وفي حالة حدوث خطأ ، يتم استدعاء الذعر.


اذهب عادة يحتوي على بادئات Must :


 // MustCompile is like Compile but panics if the expression cannot be parsed. // It simplifies safe initialization of global variables holding compiled regular // expressions. func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp } 

 // Must is a helper that wraps a call to a function returning (*Template, error) // and panics if the error is non-nil. It is intended for use in variable initializations // such as // var t = template.Must(template.New("name").Parse("html")) func Must(t *Template, err error) *Template { if err != nil { panic(err) } return t } 

تجدر الإشارة إلى شيء آخر يتعلق بالذعر والكوروتينات.


جزء من الأطروحات عما نوقش أعلاه:


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

المعالج الرئيسي لن يعترض الذعر من foo وسيتعطل البرنامج:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) } 

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


 type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) } 

التعامل / الاختيار


ربما سنرى في المستقبل تغييرات في معالجة الأخطاء. يمكنك التعرف عليهم من خلال الروابط:
go2draft
معالجة الخطأ في Go 2


هذا كل شيء لهذا اليوم. شكرا لك!

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


All Articles