שגיאות הן ערכים
פונקציית Go שיכולה להיכשל מחזירה error כתוצאה האחרונה שלה. מי שקורא בודק אותה מיד.
פלט:
parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax
זה כל המנגנון. אין exceptions, אין try או catch, ואין זרימת בקרה נסתרת: שגיאה עוברת רק לאן שהקוד שלכם מעביר אותה. המחיר הוא חזרה גלויה על if err != nil. היתרון הוא שכל נקודת כישלון גלויה בדף, ואתם מחליטים בכל אחת מה קורה.
הטיפוס error
error הוא interface מובנה עם מתודה אחת:
type error interface {
Error() string
}
כל טיפוס עם מתודת Error() string הוא שגיאה. שגיאה שהיא nil פירושה הצלחה. הדפסת שגיאה עם fmt.Println(err) או עם %v קוראת ל-Error().
יצירת שגיאות
שתי פונקציות מכסות את רוב המקרים.
errors.New יוצרת שגיאה עם טקסט קבוע. fmt.Errorf מעצבת שגיאה, עם אותם verbs כמו Printf. לפי המוסכמה, מחרוזות שגיאה מתחילות באות קטנה ואין בסופן סימן פיסוק, כי בדרך כלל הן משובצות בהודעות ארוכות יותר: load config: open app.yaml: no such file or directory.
הדפוס if err != nil
הצורה המקובלת היא: לקרוא, לבדוק, לחזור מוקדם. מסלול ההצלחה נשאר בשוליים, וכל כישלון יוצא ברגע שהוא קורה.
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, err
}
u, err := parseUser(row)
if err != nil {
return nil, err
}
if err := u.Validate(); err != nil {
return nil, err
}
return u, nil
}
שתי מוסכמות ששווה לשים לב אליהן:
- במקרה של שגיאה, החזירו את ערך האפס בשאר התוצאות (
nil,0,""). אסור למי שקורא להשתמש בהן כש-err != nil. if err := f(); err != nilמגביל את ההיקף שלerrל-ifכשהפונקציה מחזירה רק שגיאה. זה שומר על ההיקף החיצוני נקי.
הימנעו מ-else אחרי החזרת שגיאה. if err != nil { return err } else { ... } רק מזיח את מסלול ההצלחה בלי סיבה.
הוספת הקשר כשמחזירים שגיאה
שגיאה שמועברת למעלה בלי שינוי מאבדת את הסיפור של מאיפה היא הגיעה. open config.yaml: no such file or directory לא אומרת לכם איזה שלב בעליית התוכנית נכשל. הוסיפו הקשר עם fmt.Errorf וה-verb %w:
פלט:
start server: read config: open /etc/myapp/config.yaml: no such file or directory
true
כל שכבה מוסיפה את מה שהיא עשתה, וההודעה הסופית נקראת כמו שביל מראש הקריאה ועד הסיבה. הקשר טוב מציין את הפעולה ואת הקלט: parse line 12, fetch user 42. אל תוסיפו "error" או "failed" בכל רמה; ההודעה היא כבר שגיאה.
%w עוטף: הוא שומר את השגיאה המקורית בתוך החדשה, כך ש-errors.Is ו-errors.As עדיין יכולות למצוא אותה. %v רק מעתיק את הטקסט. השתמשו ב-%v כשאתם רוצים בכוונה להסתיר פרט מימוש ממי שקורא, למשל כדי שלא יתחילו להסתמך על טיפוס השגיאה של דרייבר מסד נתונים.
בדיקת שגיאות מסוימות: errors.Is ו-errors.As
לפעמים מי שקורא צריך להגיב לסוג אחד של כישלון: קובץ חסר פירושו "השתמש בברירות מחדל", timeout פירושו "נסה שוב". שתי פונקציות עונות על זה, ושתיהן מסתכלות דרך כל שכבות העטיפה.
כללי אצבע:
- השוו לערכי שגיאה מוגדרים מראש (sentinels כמו
io.EOF,os.ErrNotExist,sql.ErrNoRows) עםerrors.Is, לא עם==.==נכשל ברגע שהשגיאה עטופה. - חלצו שגיאה עם טיפוס בעזרת
errors.As, לא בעזרת type assertion, מאותה סיבה.errors.Asמקבלת מצביע למשתנה מטיפוס היעד. - אף פעם אל תתאימו לפי הטקסט של
err.Error(). הודעות משתנות בין גרסאות, והתאמה לפי טקסט נשברת בשקט כשזה קורה.
הגדרת שגיאות sentinel וטיפוסי שגיאה משלכם, ושילוב של כמה שגיאות עם errors.Join, מוסברים בדף על שגיאות מותאמות אישית.
טפלו בשגיאה פעם אחת
בשגיאה צריך לטפל בדיוק פעם אחת. טיפול פירושו אחד מאלה: להחזיר אותה (בדרך כלל עטופה), לתעד אותה ולהמשיך, לנסות שוב, או להפוך אותה לתשובה למשתמש. ביצוע של שניים מהם הוא באג השגיאות הנפוץ ביותר בקוד Go.
// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
log.Printf("could not fetch user: %v", err)
return err
}
// Right: add context and return. The top of the program logs once.
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
תיעוד והחזרה גם יחד מייצרים את אותו כישלון כמה פעמים בלוגים, כל פעם עם פחות הקשר מההודעה הסופית. תנו לשגיאות לזרום למעלה עד המקום שיכול להחליט מה לעשות (handler של HTTP, main, לולאת worker), ותעדו שם.
לאן שגיאות מגיעות בסוף
בראש התוכנית, משהו צריך לפעול לפי השגיאה. ב-main זה בדרך כלל אומר להדפיס אותה ולצאת עם סטטוס שונה מאפס:
הרצה בלי ארגומנטים מדפיסה error: usage: app <name> ל-stderr ויוצאת עם סטטוס 1 (הקלידו שם בחלונית Args כדי לראות את המסלול השני). שמירה על main בצורה הזאת, עם העבודה האמיתית בתוך run, הופכת את התוכנית לניתנת לבדיקה ומבטיחה שהוראות defer בתוך run עדיין רצות, כי os.Exit מדלגת על קריאות defer.
בשרת HTTP הראש הוא ה-handler: הוא ממפה את השגיאה לקוד סטטוס ולהודעה בטוחה ללקוח, ומתעד בשבילכם את ההודעה המפורטת.
שגיאות שמותר להתעלם מהן, ושגיאות שאסור
התעלמות משגיאה נכונה לפעמים, אבל עשו אותה מפורשת עם _ כדי שהקוראים יידעו שזו הייתה החלטה:
_ = conn.SetDeadline(t) // best effort
חלק מהקריאות לא יכולות להיכשל בפועל (strings.Builder.WriteString, bytes.Buffer.Write). אחרות נראות תמימות ואינן: Close על קובץ שכתבתם אליו יכולה לדווח שהנתונים אף פעם לא הגיעו לדיסק, ו-json.Marshal נכשלת על channels ועל פונקציות. כשיש ספק, בדקו.
ה-linter errcheck (שכלול ב-golangci-lint) מדווח על שגיאות שלא נבדקו. go vet לא מסמן אותן בעצמו.
שגיאות ו-panics
ל-Go יש גם panic, אבל זו לא מערכת exceptions. השתמשו בשגיאות לכל דבר שיכול להשתבש בפעולה רגילה: קלט שגוי, קבצים חסרים, כשלי רשת. שמרו את panic לבאגים (מצב בלתי אפשרי, אינווריאנט שבור) ולכשלים בזמן העלייה שבהם אין טעם להמשיך. ספרייה כמעט אף פעם לא צריכה לזרוק panic דרך ה-API שלה. ראו את הדף על panic ו-recover.
צמצום החזרות
if err != nil מילולי, והצעות להוסיף לו תחביר חדש נדחו שוב ושוב; צוות Go הודיע ב-2025 שהוא כבר לא מקדם שינויי תחביר לטיפול בשגיאות. כמה דפוסים מצמצמים את הרעש בתוך השפה:
- חזרו מוקדם ושמרו על פונקציות קטנות. רוב החזרות מגיעות מפונקציות ארוכות שמבצעות הרבה שלבים.
- השגיאה הדביקה (sticky error). ברצף של כתיבות, שמרו את השגיאה הראשונה בשדה של struct והפכו קריאות מאוחרות יותר לפעולות ריקות ברגע שהיא מוצבת.
bufio.Writerעובד כך: בודקים את השגיאה פעם אחת אחריFlush. - עטפו פעם אחת בכל פונקציה. closure שנדחה על תוצאה בעלת שם יכול להוסיף את אותו הקשר לכל שגיאה שהפונקציה מחזירה (ראו defer).
טעויות נפוצות
- שימוש בערך כש-err אינו nil. קודם בודקים, אחר כך משתמשים.
- לתעד וגם להחזיר. בחרו אחד.
- השוואה עם
==אחרי עטיפה. השתמשו ב-errors.Is. - איבוד הסיבה עם
%v. השתמשו ב-%w, אלא אם ההסתרה היא המטרה. - החזרת מצביע nil עם טיפוס בתור
error.var e *MyErr; return eאינו nil מבחינת מי שקורא. החזירוnilמפורש. - הודעות עם אות גדולה או סימן פיסוק.
errors.New("Failed to connect.")נקראת רע אחרי עטיפה. כתבוconnect to db: ....
שאלות נפוצות
איך עובד טיפול בשגיאות ב-Go?
פונקציות שיכולות להיכשל מחזירות error כתוצאה האחרונה שלהן. מי שקורא בודק אותה מיד: v, err := f(); if err != nil { return err }. error הוא ערך interface רגיל עם מתודה אחת, Error() string, ו-nil פירושו הצלחה. אין exceptions.
האם יש ב-Go try/catch?
לא. ב-Go אין exceptions ואין try/catch. כישלונות צפויים מוחזרים כערכי error ונבדקים עם if err != nil. panic ו-recover קיימים, אבל הם נועדו לבאגים בתוכנה ולמצבים שאי אפשר להתאושש מהם, לא לזרימת שגיאות רגילה.
איך מחזירים שגיאה ב-Go?
הצהירו על error כתוצאה האחרונה והחזירו nil במקרה של הצלחה. צרו שגיאות עם errors.New("message") לטקסט קבוע, או עם fmt.Errorf("reading %s: %w", name, err) כדי להוסיף הקשר לשגיאה שקיבלתם. במקרה של כישלון, החזירו ערכי אפס בשאר התוצאות.
מה ההבדל בין %w ל-%v ב-fmt.Errorf?
שניהם מכניסים את ההודעה של השגיאה המקורית לשגיאה החדשה. %w גם עוטף אותה, כך ש-errors.Is ו-errors.As עדיין יכולות למצוא את המקורית. %v מייצר שגיאה חדשה עם הטקסט בלבד. השתמשו ב-%w כשמי שקורא עשוי להצטרך לבדוק את הסיבה, וב-%v כשאתם רוצים להסתיר אותה.
איך בודקים איזו שגיאה הוחזרה ב-Go?
השתמשו ב-errors.Is(err, target) כדי להשוות מול שגיאת sentinel כמו io.EOF או os.ErrNotExist, וב-errors.As(err, &target) כדי לחלץ טיפוס שגיאה מסוים כמו *fs.PathError. שתיהן עוברות דרך שגיאות עטופות. הימנעו מהשוואת מחרוזות של err.Error().