פונקציות אנונימיות
ליטרל פונקציה הוא פונקציה בלי שם. הוא ביטוי, ולכן אפשר לשים אותו בכל מקום שבו אפשר לשים ערך: משתנה, ארגומנט, ערך מוחזר, שדה של 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 בתוך הלולאה כפתרון עוקף; הוא לא מזיק, אבל כבר לא נחוץ.