Menu

Closures ופונקציות אנונימיות ב-Golang, עם דוגמאות

פונקציות אנונימיות ב-Go יכולות ללכוד משתנים מההיקף שסביבן. כך נוצרים closures: מונים, גנרטורים, middleware ו-callbacks ששומרים על מצב משלהם.

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

פונקציות אנונימיות

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

ל-Go אין קיצור של חץ או lambda. הצורה המלאה func(params) results { body } היא הדרך היחידה לכתוב פונקציה כזאת.

לכידת משתנים

ליטרל פונקציה יכול להשתמש במשתנים של הפונקציה העוטפת. הוא לא מעתיק אותם: ה-closure והקוד שסביבו חולקים משתנה אחד.

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

גנרטורים ומונים

החזרת closure מפונקציה נותנת לכל פונקציה מוחזרת מצב פרטי משלה.

כל קריאה ל-counter() יוצרת n חדש, ולכן c1 ו-c2 סופרים באופן עצמאי. שום דבר מבחוץ לא יכול לקרוא את n או לאפס אותו, וזו צורה קטנה של כימוס (encapsulation).

Closures בלולאות (Go 1.22 ומעלה)

לפני Go 1.22, ללולאת for היה משתנה לולאה אחד לכל הלולאה. כל ה-closures שנוצרו בתוכה לכדו את אותו משתנה יחיד וראו את הערך הסופי שלו. זה היה באג ה-closures הנפוץ ביותר ב-Go.

החל מ-Go 1.22, כל איטרציה מקבלת עותק משלה, ולכן הקוד הבא מדפיס 0 1 2:

ב-Go 1.21 ומטה, אותה לולאה שנכתבה כ-for i := 0; i < 3; i++ (גם range על מספר שלם הוא חדש ב-1.22) הדפיסה 3 3 3. הכלל החדש חל לפי מודול, על סמך שורת ה-go ב-go.mod, כך שמודול ישן עדיין מקבל את ההתנהגות הישנה עד ששורת ה-go שלו מועלית ל-1.22 ומעלה. עדיין תראו i := i או v := v בתוך לולאות בקוד ישן: זה היה התיקון הידני, ועכשיו הוא מיותר.

הכלל של עותק לכל איטרציה חל רק על משתנים שהוצהרו בהצהרת ה-for עצמה. משתנה שהוצהר לפני הלולאה ומתעדכן בגוף שלה הוא עדיין משתנה משותף אחד:

var last string
for _, s := range items {
	last = s
	handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}

Closures ו-goroutines

goroutine שמופעלת עם ליטרל פונקציה היא גם closure. אותם כללי שיתוף חלים, ועוד כלל המקביליות הרגיל: אם כמה goroutines כותבות למשתנה לכוד, צריך mutex או channel.

הקוד הזה תמיד מדפיס 5050. כל goroutine קוראת את ה-i שלה (סמנטיקת הלולאות של Go 1.22) וכותבת ל-total המשותף תחת נעילה. הסירו את ה-mutex והתוצאה הופכת לבלתי צפויה; go run -race מדווח על כך כ-data race. ראו goroutines לגבי WaitGroup וה-race detector.

איפה closures מופיעים בקוד אמיתי

  • מיון וחיפוש: slices.SortFunc, slices.IndexFunc ו-sort.Slice כולן מקבלות ליטרל פונקציה.
  • ניקוי נדחה: defer func() { ... }() מריץ בלוק ביציאה מהפונקציה ויכול לקרוא את המשתנים שלה. ראו defer.
  • HTTP middleware: פונקציה שמקבלת handler ומחזירה ליטרל http.HandlerFunc חדש שעוטף אותו.
  • קונפיגורציה: functional options (func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) הם closures על הארגומנט.

דוגמת middleware שלוכדת גם פרמטר וגם מונה:

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

  • לצפות לתמונת מצב. closure קורא את הערך הנוכחי של המשתנה כשהוא רץ, לא את הערך שהיה כשה-closure נוצר. העבירו את הערך כפרמטר אם אתם צריכים תמונת מצב: go func(v int) { ... }(x).
  • ליטרלים רקורסיביים. ליטרל פונקציה לא יכול להתייחס לעצמו דרך המשתנה שאליו הוא מושם באותו :=. הצהירו קודם על המשתנה: var walk func(n int); walk = func(n int) { ... walk(n-1) }.
  • כתיבות לא מסונכרנות מ-goroutines. לכידה היא לא סנכרון. הגנו על כתיבות משותפות.

שאלות נפוצות

מהי פונקציה אנונימית ב-Go?

ליטרל פונקציה בלי שם: func(x int) int { return x * 2 }. אפשר להשים אותה למשתנה, להעביר אותה כארגומנט, להחזיר אותה או לקרוא לה מיד על ידי הוספת () אחרי הסוגר המסולסל הסוגר. ל-Go אין תחביר lambda נפרד; ליטרל פונקציה הוא ה-lambda.

מהו closure ב-Go?

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

האם closures ב-Go לוכדים משתנים לפי ערך או לפי הפניה?

לפי הפניה. ה-closure והקוד שסביבו משתמשים באותו משתנה. אם אתם צריכים תמונת מצב, העתיקו את הערך למשתנה חדש לפני יצירת ה-closure, או העבירו אותו כארגומנט.

האם באג משתנה הלולאה ב-closures תוקן ב-Go?

כן, החל מ-Go 1.22. כל איטרציה של לולאת for מצהירה עכשיו על משתנה לולאה חדש, כך ש-closures ו-goroutines שנוצרים בלולאה רואים את הערך של אותה איטרציה. זה חל על מודולים שה-go.mod שלהם מציין go 1.22 ומעלה. בקוד ישן יותר מופיע לעיתים קרובות i := i בתוך הלולאה כפתרון עוקף; הוא לא מזיק, אבל כבר לא נחוץ.

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

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

להתחיל