Menu

שגיאות ב-Golang: עטיפה, errors.Is, errors.As וטיפוסים מותאמים

הגדירו שגיאות sentinel וטיפוסי שגיאה מותאמים, עטפו שגיאות עם %w, בדקו אותן עם errors.Is ו-errors.As, שלבו כמה שגיאות עם errors.Join, וכתבו מתודות Unwrap ו-Is כשצריך.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שלושה סוגי שגיאות

קוד Go משתמש בשלוש צורות של שגיאות, מהפשוטה לעשירה:

  1. שגיאה אד הוק: errors.New("...") או fmt.Errorf("...") שנוצרת במקום. הקוראים יכולים רק לקרוא את ההודעה.
  2. שגיאת sentinel: משתנה ברמת החבילה כמו io.EOF. הקוראים יכולים לבדוק אותה עם errors.Is.
  3. טיפוס שגיאה: struct עם מתודת Error(), שנושא שדות. הקוראים יכולים לחלץ אותו עם errors.As ולקרוא את השדות.

בחרו את הפשוטה ביותר שמאפשרת לקוראים לעשות את מה שהם צריכים.

שגיאות sentinel

sentinel הוא ערך שגיאה שמוצהר פעם אחת ומושווה לפי זהות. לפי המוסכמה השמות מתחילים ב-Err.

פלט:

mug bought
cap is out of stock, notify me later
buy "hat": not found

שתי קריאות ל-errors.New עם אותו טקסט יוצרות שגיאות שונות: errors.New("x") == errors.New("x") הוא false. לכן sentinel חייב להיות משתנה משותף יחיד.

sentinels הופכים לחלק מה-API של החבילה שלכם. ברגע שקוראים בודקים את ErrNotFound, אי אפשר להפסיק להחזיר אותה בלי לשבור אותם. ייצאו רק את אלה שהקוראים באמת צריכים כדי להסתעף לפיהם.

טיפוסי שגיאה מותאמים

כשמי שקורא צריך פרטים (איזה שדה, איזה קוד סטטוס, כמה זמן לחכות לפני ניסיון חוזר), הגדירו טיפוס.

פלט:

register: age: must be between 0 and 150
bad field: age

למתודה יש receiver מסוג מצביע והפונקציה מחזירה &ValidationError{...}, ולכן היעד של errors.As הוא *ValidationError, ומעבירים את הכתובת שלו (&ve, מטיפוס **ValidationError). טעות ברמת ההפניה הזאת היא הטעות הקלאסית עם errors.As: עם receiver מסוג ערך הייתם מחזירים ValidationError{...} ומצהירים var ve ValidationError. go vet תופס את הפספוס הנפוץ ביותר, העברת ve במקום &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.

עטיפה עם %w

fmt.Errorf עם %w מחזירה שגיאה שזוכרת את זו שהיא עוטפת. כל שכבה מוסיפה הקשר ושומרת על השרשרת שלמה.

פלט:

start: connect db: timeout
  *fmt.wrapError: start: connect db: timeout
  *fmt.wrapError: connect db: timeout
  *errors.errorString: timeout
true
false

עם %v ההודעה זהה אבל השרשרת נחתכת, ולכן errors.Is מחזירה false. בחרו ב-%w כשהקוראים צריכים לראות את הסיבה, וב-%v כשהסיבה היא פרט מימוש שאתם לא רוצים שיסתמכו עליו.

החל מ-Go 1.20, קריאה אחת יכולה לעטוף כמה שגיאות: fmt.Errorf("%w; %w", err1, err2). errors.Is מתאימה אז לכל אחת מהן.

errors.Join: כמה שגיאות בבת אחת

ולידציה וניקוי מייצרים לעיתים קרובות יותר משגיאה אחת. errors.Join (Go 1.20) מאגדת אותן.

פלט:

name: required
email: required
age: negative
---
true
true

errors.Join מחזירה nil כשכל הארגומנטים הם nil, ולכן הפונקציה שלמעלה לא צריכה טיפול מיוחד במקרה של "אין שגיאות".

המתודות Unwrap ו-Is

errors.Is ו-errors.As מוצאות שגיאות עטופות על ידי קריאה למתודת Unwrap. טיפוס מותאם שמחזיק סיבה צריך לחשוף אותה:

טיפוס שעוטף כמה שגיאות מממש במקום זאת את Unwrap() []error.

טיפוס יכול גם להגדיר Is(target error) bool כדי להחליט בעצמו על שוויון, למשל כדי להתאים לכל *HTTPError עם אותו קוד סטטוס. לעיתים רחוקות צריך את זה; עטיפת sentinel והחזרתו מ-Unwrap בדרך כלל עושות את העבודה.

מלכודת ה-nil עם טיפוס

אף פעם אל תצהירו על משתנה שגיאה עם הטיפוס הקונקרטי שלכם ותחזירו אותו דרך error:

func check() error {
	var err *ValidationError // nil pointer
	// ... no problem found
	return err // non-nil error! Its type is *ValidationError
}

ה-if err != nil של מי שקרא יהיה true, כי interface שמחזיק מצביע nil עם טיפוס אינו nil. החזירו nil מפורש במקרה של הצלחה, והשאירו משתני שגיאה מקומיים מטיפוס error. הדף על interfaces מסביר למה.

איזה סוג לבחור

הקוראים צריכיםספקו
רק לתעד או להציג את הכישלוןfmt.Errorf("...: %w", err)
להסתעף לפי מצב מסוים אחדsentinel var ErrX = errors.New(...)
לקרוא פרטים על הכישלוןטיפוס שגיאה עם שדות
לראות כמה כישלונות בלתי תלוייםerrors.Join

טעויות נפוצות

  • השוואת שגיאות עטופות עם ==. השתמשו ב-errors.Is.
  • העברת ערך שאינו מצביע ל-errors.As. היא צריכה מצביע למשתנה מטיפוס היעד.
  • יצירת "sentinel" בתוך פונקציה. return errors.New("not found") יוצר ערך חדש בכל קריאה; הקוראים לא יכולים להשוות מולו.
  • ייצוא כל שגיאה. כל sentinel או טיפוס מיוצא הוא הבטחה של API.
  • התאמה לפי err.Error(). מחרוזות נועדו לבני אדם.

שאלות נפוצות

איך יוצרים שגיאה מותאמת אישית ב-Go?

למצב קבוע, הצהירו על sentinel ברמת החבילה: var ErrNotFound = errors.New("not found"). לשגיאה שנושאת נתונים, הגדירו טיפוס עם מתודת Error() string: type ValidationError struct { Field string } ו-func (e *ValidationError) Error() string { return e.Field + " is invalid" }.

איך עוטפים שגיאה ב-Go?

השתמשו ב-fmt.Errorf עם ה-verb %w: return fmt.Errorf("load user %d: %w", id, err). ההודעה של השגיאה החדשה כוללת את הישנה, ו-errors.Unwrap, errors.Is ו-errors.As יכולות להגיע למקורית. החל מ-Go 1.20 קריאה אחת ל-Errorf יכולה לעטוף כמה שגיאות עם כמה verbs של %w.

מה ההבדל בין errors.Is ל-errors.As?

errors.Is(err, target) עונה על השאלה "האם ערך השגיאה המסוים הזה נמצא איפשהו בשרשרת", עבור sentinels כמו io.EOF. errors.As(err, &target) עונה על השאלה "האם יש בשרשרת שגיאה מהטיפוס הזה", ואם כן, שומרת אותה ב-target כדי שתוכלו לקרוא את השדות שלה.

מה עושה errors.Join?

errors.Join(errs...) (Go 1.20) משלבת כמה שגיאות לשגיאה אחת. ההודעה שלה היא ההודעות הנפרדות מופרדות בשורות חדשות, שגיאות nil מושמטות, והיא מחזירה nil אם כולן nil. errors.Is ו-errors.As מתאימות אם אחת מהשגיאות המשולבות מתאימה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל