Menu

defer ב-Golang: סדר ביצוע, הערכת ארגומנטים ומלכודות

defer מתזמן קריאה שתרוץ כשהפונקציה העוטפת חוזרת. למדו על סדר LIFO, מתי הארגומנטים מוערכים, סגירת קבצים ושחרור mutexes, defer בתוך לולאות ושינוי תוצאות בעלות שם.

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

מה defer עושה

defer דוחף קריאה לפונקציה לרשימה. כשהפונקציה העוטפת חוזרת, הרשימה רצה בסדר הפוך.

פלט:

start
end
deferred 3
deferred 2
deferred 1

הסדר הוא האחרון שנכנס יוצא ראשון, כמו מחסנית. זה מתאים לאופן שבו משאבים מקוננים: אם פותחים את A ואחר כך את B, בדרך כלל רוצים לסגור את B לפני A.

ניקוי ליד ההשגה

השימוש העיקרי ב-defer הוא לכתוב את הניקוי בשורה שמיד אחרי השגת המשאב, כך ששום מסלול חזרה לא ישכח אותו.

שימו לב לסדר: קודם בודקים את השגיאה, ורק אחר כך defer. אם os.Open נכשלה, f הוא nil, ודחיית f.Close() לפני הבדיקה הייתה קוראת ל-Close על *os.File שהוא nil (מה שמחזיר שגיאה שאף פעם לא תראו, ומבלבל את מי שקורא את הקוד).

אותה צורה עובדת לנעילות:

mu.Lock()
defer mu.Unlock()

אם הקוד שביניהן נכנס ל-panic, ה-mutex עדיין משתחרר.

ארגומנטים מוערכים מיד

הפונקציה שנדחתה והארגומנטים שלה מוערכים כשהוראת ה-defer רצה. רק הקריאה עצמה מחכה.

פלט:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) לכדה את הערך 1 בשורת ה-defer. ל-closure אין ארגומנטים; הוא קורא את x כשהוא סוף סוף רץ. בחרו בצורה שמתאימה למה שאתם רוצים לתעד.

זה חל גם על receivers של מתודות. defer t.Stop() מעריך את t מיד, כך שהשמה מחדש ל-t בהמשך לא משנה איזה ערך ייעצר.

טריק מדידת זמן נפוץ משתמש בכלל הזה בכוונה:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") נקראת מיד (היא יכולה להדפיס "enter" ולשמור את זמן ההתחלה), והפונקציה שהיא מחזירה היא זו שנדחית.

defer בתוך לולאה

קריאות defer רצות כשהפונקציה חוזרת, לא בסוף כל איטרציה של הלולאה. בלולאה על הרבה קבצים, זה משאיר כל קובץ פתוח עד שהפונקציה מסתיימת.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

עם אלפי נתיבים זה מכלה את ה-file descriptors. העבירו את גוף הלולאה לפונקציה משלו כדי שכל defer ירוץ בכל איטרציה:

כל קריאה ל-processFile סוגרת את הקובץ שלה לפני שהבא נפתח. ליטרל פונקציה שנקרא במקום (func() { ... }()) עובד באותו אופן, כשפונקציית עזר עם שם נראית מוגזמת.

שינוי ערכים מוחזרים

closure שנדחה רץ אחרי שהוראת ה-return השימה את התוצאות, והוא יכול לשנות תוצאות בעלות שם לפני שמי שקרא רואה אותן.

הקוד מדפיס 10 ו-save failed: disk full. עם תוצאה בלי שם, פונקציה שנדחתה עדיין יכולה לרוץ, אבל אין לה דרך לשנות את מה שמוחזר.

תפיסת השגיאה מ-Close

defer f.Close() זורק את השגיאה של Close. לקבצים שרק קוראים מהם, זה בסדר. לקבצים שכתבתם אליהם, Close יכולה לדווח על כישלון בכתיבה הסופית, ולכן השגיאה חשובה. תוצאה בעלת שם מאפשרת לשמור אותה:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) היא חלופה קצרה יותר כשרוצים לדווח על שתי השגיאות.

defer, panic ו-recover

קריאות defer רצות בזמן ש-panic מגולל את המחסנית. זה המקום היחיד שבו recover עושה משהו, וכך שרת מונע מבקשה פגומה אחת להפיל את התהליך. הפרטים נמצאים בדף על panic ו-recover.

קריאות defer לא רצות כשהתוכנית יוצאת דרך os.Exit או log.Fatal. אם main דוחה ניקוי ואז קוראת ל-os.Exit(1), הניקוי מדולג.

עלות

החל מ-Go 1.14, רוב ה-defers מוטמעים ישירות בקוד (open-coded) על ידי הקומפיילר ועולים כמה ננו-שניות. שימוש ב-defer לכל שחרור mutex וסגירת קובץ הוא הסגנון הרגיל. defers בתוך לולאות הם היוצא מן הכלל: אי אפשר להטמיע אותם כך והם עוברים למסלול איטי יותר, וזו עוד סיבה להעביר גופי לולאות לפונקציות.

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

  • defer לפני בדיקת השגיאה. בדקו קודם את err מ-Open, ורק אז defer Close.
  • לצפות לניקוי בכל איטרציה בלולאה. defers רצים ביציאה מהפונקציה.
  • לצפות שארגומנט שנדחה יראה שינויים מאוחרים יותר. הארגומנטים נקבעים בשורת ה-defer. השתמשו ב-closure כדי לקרוא ביציאה.
  • להסתמך על defer עם os.Exit. הוא אף פעם לא רץ.

שאלות נפוצות

מה עושה defer ב-Go?

defer f() מתזמן את f() כך שתרוץ כשהפונקציה העוטפת חוזרת, בין אם היא חוזרת כרגיל, דרך return מוקדם, או בגלל panic. משתמשים בו כדי לשים את הניקוי (סגירת קובץ, שחרור mutex) ממש ליד הקוד שהשיג את המשאב.

באיזה סדר רצות קריאות defer ב-Go?

האחרון שנכנס יוצא ראשון (LIFO). הקריאה שנדחתה אחרונה רצה ראשונה. defer fmt.Println(1); defer fmt.Println(2) מדפיס 2 ואז 1.

מתי מוערכים הארגומנטים של פונקציה שנדחתה עם defer?

מיד, כשהוראת ה-defer מתבצעת, ולא כשהקריאה רצה. x := 1; defer fmt.Println(x); x = 2 מדפיס 1. כדי לקרוא את הערך בזמן היציאה, דחו closure: defer func() { fmt.Println(x) }().

האם defer רץ בזמן panic או os.Exit?

קריאות defer רצות בזמן ש-panic מגולל את המחסנית, ולכן recover עובדת בתוכן. הן לא רצות כשהתוכנית קוראת ל-os.Exit (או ל-log.Fatal, שקוראת לה), והן לא רצות ב-goroutines אחרות כש-main חוזרת.

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

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

להתחיל