איך panic נראה
panic עוצר את הפונקציה הנוכחית, מריץ את הקריאות שלה שנדחו עם defer, ואז עושה את אותו הדבר בפונקציה שקראה לה, וכך הלאה במעלה המחסנית. אם הוא מגיע לראש ה-goroutine, התוכנית קורסת.
התוכנית הזאת יוצאת עם סטטוס 2. הפלט הוא:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
הקריאה שנדחתה רצה לפני דוח הקריסה. ה-trace מציין את ה-goroutine, את הפונקציה ואת השורה, ובדרך כלל זה מספיק כדי למצוא את הבאג.
panics נפוצים בזמן ריצה
| הודעה | סיבה |
|---|---|
index out of range [5] with length 3 | אינדקס של slice, מערך או מחרוזת אחרי הסוף |
slice bounds out of range [:7] with capacity 5 | חיתוך אחרי ה-capacity |
invalid memory address or nil pointer dereference | קריאת שדה או קריאה למתודה דרך pointer שהוא nil |
assignment to entry in nil map | כתיבה ל-map שאף פעם לא נוצר |
interface conversion: interface {} is int, not string | type assertion בצורת ערך יחיד לטיפוס הלא נכון |
integer divide by zero | חילוק או מודולו של מספר שלם ב-0 (ב-floats מקבלים במקום זה +Inf או NaN) |
close of closed channel, send on closed channel | שימוש לא נכון ב-channel |
all goroutines are asleep - deadlock! | כל ה-goroutines חסומות (שגיאה פטאלית, לא panic) |
כל אחד מאלה הוא באג בתוכנית, לא מצב שצריך לטפל בו. התיקון הוא בדיקת גבולות, בדיקת nil, make או assertion עם comma-ok, לא recover.
התאוששות עם recover
recover() עוצרת panic. היא עובדת רק כשקוראים לה ישירות בתוך פונקציה שנדחתה עם defer, כי פונקציות כאלה הן הקוד היחיד שרץ בזמן ש-panic מגולל את המחסנית.
פלט:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
מה קרה בקריאה השנייה:
a / bנכנס ל-panic.- ה-closure שנדחתה רצה, ו-
recover()החזירה את ערך ה-panic (runtime.Error). - הגלילה נעצרה.
safeDivideחזרה כרגיל ל-main, עם התוצאה בעלת השםerrשה-closure קבעה.
התוצאה בעלת השם היא מה שמאפשר לפונקציה שנדחתה להחזיר שגיאה. בלעדיה, הפונקציה מחזירה את ערכי האפס שלה. העמוד על defer מסביר איך closures שנדחו משנות תוצאות.
recover() מחזירה nil כשאין panic, ולכן הבדיקה if r != nil הופכת את הפונקציה שנדחתה לבלתי מזיקה במסלול הרגיל. אם קוראים ל-recover מחוץ לפונקציה שנדחתה, או בפונקציה שהפונקציה שנדחתה קוראת לה, היא מחזירה nil ולא עושה כלום.
panic עם ערך משלכם
panic מקבלת כל ערך. שגיאה או מחרוזת הן הבחירה הרגילה.
בצעו recover למה שאתם מצפים לו, והפעילו panic מחדש לכל דבר אחר. בליעה של כל panic מסתירה באגים אמיתיים.
מאז Go 1.21, panic(nil) הופך ל-*runtime.PanicNilError, כך ש-recover() שמחזירה nil אומרת עכשיו באופן אמין "אין panic".
panics ב-goroutines
recover תופסת רק panics ב-goroutine שלה. panic בכל goroutine בלי recover הורג את כל התהליך, כולל main וכל שאר ה-goroutines.
שתי השורות של ה-workers יכולות להופיע בכל סדר; main finished תמיד מגיעה אחרונה. defer recover() ב-main לא היה מציל את התוכנית מה-worker השני. זו הסיבה ששרתי HTTP מבצעים recover לכל בקשה: net/http תופסת panics בכל goroutine של handler, רושמת אותם ללוג וסוגרת את החיבור הזה, כך שבקשה גרועה אחת לא מפילה את השרת.
חלק מהכישלונות הם שגיאות פטאליות, לא panics, ואי אפשר להתאושש מהם בכלל: concurrent map writes, מחסור בזיכרון, וה-all goroutines are asleep של מזהה ה-deadlock.
מתי panic הוא הבחירה הנכונה
הכלל הכללי של Go: החזירו שגיאות לכל דבר שיכול להשתבש בזמן ריצה, והשתמשו ב-panic רק לטעויות של המתכנת. באופן קונקרטי, panic מתאים כש:
- אינווריאנט נשבר.
switchעל enum משלכם מגיע למקרה שלא יכול לקרות. המשך הריצה היה משחית נתונים. - פונקציית עזר מסוג
Mustמקבלת קלט קבוע לא תקין.regexp.MustCompile,template.Mustו-uuid.MustParseעוטפות פונקציה שמחזירה שגיאה ונכנסות ל-panic בכישלון. השתמשו בהן לערכים שידועים בזמן קומפילציה, בדרך כלל משתנים ברמת החבילה, שבהם כישלון אומר שקוד המקור שגוי:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- האתחול לא יכול להמשיך. הגדרה נדרשת חסרה ב-
main. גם כאן, הדפסת השגיאה וקריאה ל-os.Exit(1)לרוב נקיות יותר מ-stack trace.
panic הוא הכלי הלא נכון ל:
- כישלונות צפויים: קלט לא תקין ממשתמש, קובץ חסר, timeout. החזירו
error; ראו טיפול בשגיאות. - בקרת זרימה: שימוש ב-panic וב-recover כמו ב-exceptions לאורך עץ קריאות גדול הופך את הקוד לקשה למעקב. הספרייה הסטנדרטית עושה את זה פנימית בכמה מקומות (ה-encoder של
encoding/json), ותמיד מבצעת recover לפני שהיא חוזרת, כך ששום panic לא יוצא מהחבילה. - APIs של ספריות: ספרייה שנכנסת ל-panic על קלט לא תקין מכריחה כל קוד קורא להוסיף recovers. החזירו שגיאה.
טעויות נפוצות
- קריאה ל-recover מחוץ לפונקציה שנדחתה. היא מחזירה
nil. - recover ב-
mainבשביל panic של goroutine. כל goroutine צריכה recover משלה. - בליעה של כל ה-panics. רשמו ללוג עם המחסנית (
debug.Stack()מ-runtime/debug) והפעילו panic מחדש למה שלא ציפיתם לו. - שימוש ב-
recoverכדי לטפל בכתיבות ל-map שהוא nil או באינדקסים מחוץ לטווח. תקנו את הבאג במקום זה.
שאלות נפוצות
מה זה panic ב-Go?
כישלון בזמן ריצה שעוצר את הזרימה הרגילה של ה-goroutine הנוכחית. Go מריצה את הקריאות שנדחו עם defer של כל פונקציה במחסנית, מהפנימית ביותר כלפי חוץ, ואם שום דבר לא מבצע recover, התוכנית מדפיסה את ערך ה-panic ו-stack trace ויוצאת עם סטטוס 2. panics מגיעים מבאגים (אינדקס מחוץ לטווח, גישה דרך pointer שהוא nil, כתיבה ל-map שהוא nil) או מקריאה מפורשת ל-panic(v).
איך מתאוששים מ-panic ב-Go?
קראו ל-recover() בתוך פונקציה שנדחתה עם defer: defer func() { if r := recover(); r != nil { ... } }(). היא מחזירה את הערך שהועבר ל-panic ועוצרת את הגלילה, כך שהפונקציה שעשתה את ה-defer חוזרת כרגיל לקוד שקרא לה. בכל מקום אחר, recover מחזירה nil ולא עושה כלום.
אפשר לבצע recover ל-panic מ-goroutine אחרת?
לא. recover עוצרת רק panic ב-goroutine שבה היא רצה. panic ב-goroutine שהפעלתם, בלי recover בתוך אותה goroutine, מפיל את כל התוכנית. כל goroutine שעלולה להיכנס ל-panic צריכה recover משלה עם defer.
מתי להשתמש ב-panic במקום להחזיר שגיאה?
לבאגים ולמצבים בלתי אפשריים, לא לכישלונות צפויים. קלט לא תקין, קבצים חסרים ושגיאות רשת הם שגיאות. panic סביר כשאינווריאנט נשבר, כשפונקציית עזר מסוג Must מקבלת קבוע שאמור תמיד להיות תקין (regexp.MustCompile), או כשהתוכנית לא יכולה להתחיל בכלל. ספריות לא צריכות לתת ל-panics לצאת מה-API הציבורי שלהן.