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

להתחיל