range היא הצורה של for שעוברת על אוסף. היא נותנת ערך אחד או שניים בכל איטרציה, ומה הערכים האלה תלוי בטיפוס שעליו עושים range:
| אופרנד | ערך ראשון | ערך שני | הערות |
|---|---|---|---|
slice []T, מערך [n]T | אינדקס int | איבר T, עותק | |
pointer למערך *[n]T | אינדקס | איבר | |
| מחרוזת | אינדקס של בייט int | rune | מפענחת UTF-8 |
map map[K]V | מפתח K | ערך V | סדר אקראי |
channel chan T | איבר T | אין | עד שה-channel נסגר |
מספר שלם n | מ-0 עד n פחות 1 | אין | Go 1.22 |
func(yield func() bool) | אין | אין | Go 1.23 |
func(yield func(V) bool) | V | אין | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 1.23 |
לבחור אילו משתנים מקבלים
for i, v := range s {} // both
for i := range s {} // index (or key) only
for _, v := range s {} // value only
for range s {} // neither: just repeat len(s) times
הצהרה על משתנה שלא משתמשים בו היא שגיאת קומפילציה, אז השמיטו או החליפו ב-_ את מה שלא צריך. for range 3 {} היא הדרך הקצרה ביותר לחזור על משהו שלוש פעמים.
הערך הוא עותק
בכל איטרציה, האיבר מועתק למשתנה הערך. שינוי המשתנה לא משנה את האוסף:
העתקה גם עולה זמן כשהאיברים הם structs גדולים. ב-slice של structs גדולים, for i := range s עם s[i] חוסך את ההעתקה.
מחרוזות מחזירות runes
range על מחרוזת מפענח UTF-8. הערך הראשון הוא ההיסט בבייטים שבו כל תו מתחיל, ולכן הוא יכול לדלג על מספרים:
כדי לעבור על בייטים במקום על תווים, השתמשו בלולאה עם אינדקס, for i := 0; i < len(s); i++, או עשו range על []byte(s). ראו את העמוד על runes ו-bytes לפרטים על UTF-8.
Maps: סדר אקראי
סדר המעבר על map לא מוגדר, וה-runtime הופך אותו לאקראי כדי שקוד לא יתחיל להסתמך על סדר אחד. לפלט דטרמיניסטי, מיינו את המפתחות:
מחיקת רשומות בזמן range בטוחה, ורשומה שנמחקה לפני שהלולאה הגיעה אליה לא מופיעה. הוספת רשומות מותרת אבל לא צפויה: מפתח חדש עשוי להופיע מאוחר יותר באותה לולאה ועשוי שלא. fmt.Println של map מדפיס את המפתחות ממוינים, וזו הסיבה שהשורה האחרונה יציבה.
Channels: עד שנסגרים
range על channel מקבל ערכים עד שה-channel נסגר ומתרוקן. אם אף אחד לא סוגר אותו, הלולאה נחסמת לנצח (ואם כל ה-goroutines חסומות, ה-runtime עוצר עם fatal error: all goroutines are asleep - deadlock!):
השולח סוגר את ה-channel, אף פעם לא המקבל. ראו את העמוד על channels ל-buffered channels ול-select.
מספרים שלמים (Go 1.22)
range n סופרת מ-0 ועד n, לא כולל. הטיפוס של משתנה הלולאה הוא הטיפוס של n:
for i := range 3 {} // i is int: 0, 1, 2
for i := range uint8(3) {} // i is uint8
for range 0 {} // runs zero times
גם n שלילי רץ אפס פעמים. n מחושב פעם אחת, לפני שהלולאה מתחילה.
מה range מחשבת, ומתי
הביטוי שאחרי range מחושב פעם אחת, לפני האיטרציה הראשונה. ב-slice, range משתמשת אחר כך ב-header של אותו slice, כך שהאורך שלו קבוע:
ה-99 שנוסף אף פעם לא מבוקר, כי אורך הלולאה נקבע על 3. אבל nums[2] = 30 כן נראה, כי בנקודה הזאת nums עדיין חולק את מערך הבסיס שלו עם ה-slice שעליו עושים range. החליפו את סדר שתי השורות וגם ה-30 נעלם: ה-append עובר את ה-capacity של 3, מקצה מערך חדש, והכתיבה נוחתת שם. range על ערך של מערך מעתיק קודם את כל המערך, ולכן השינוי ב-arr[2] לא נראה; עשו range על &arr או על arr[:] כדי להימנע מההעתקה.
range על פונקציות (Go 1.23)
מאז Go 1.23, range מקבלת גם פונקציות iterator. iterator מקבל callback בשם yield, קורא לו פעם אחת לכל ערך, ועוצר כש-yield מחזירה false (מה שקורה כשגוף הלולאה מבצע break או return):
iter.Seq[V] ו-iter.Seq2[K, V] הם השמות המקובלים לשתי צורות ה-iterator. הספרייה הסטנדרטית מחזירה אותם מ-slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All, וב-Go 1.24 גם מ-strings.SplitSeq, strings.Lines ו-bytes.SplitSeq. ה-iterator חייב לכבד את ערך ההחזרה של yield: קריאה נוספת ל-yield אחרי שהיא החזירה false נכנסת ל-panic.
משתני לולאה הם לכל איטרציה (Go 1.22)
מאז Go 1.22, המשתנים שמוצהרים ב-for ... range חדשים בכל איטרציה. לכידה של v ב-closure או ב-goroutine לוכדת את הערך של אותה איטרציה, ולא משתנה משותף שבסוף מחזיק את האיבר האחרון. לפני Go 1.22 הייתם צריכים v := v בתוך הלולאה; השורה הזאת עכשיו מיותרת. הכלל נקבע לפי גרסת ה-go ב-go.mod, כך שמודול ישן שומר על ההתנהגות הישנה.
טעויות נפוצות
- לקיחת הכתובת של משתנה הערך. לפני Go 1.22,
ptrs = append(ptrs, &v)שמר את אותה כתובת בכל פעם. מאז 1.22 הוא שומר כתובות שונות, אבל כל אחת מצביעה על עותק, לא על האיבר שב-slice. השתמשו ב-&items[i]כדי להצביע לתוך ה-slice. - ציפייה לסדר ב-map. פלט שנראה ממוין בבדיקה קטנה לא יישאר ממוין.
- range על channel שאף אחד לא סוגר. הלולאה, וה-goroutine שמריצה אותה, מחכות לנצח.
- range על מערך גדול לפי ערך. כל המערך מועתק קודם. עשו range על slice שלו במקום זה.
שאלות נפוצות
מה range מחזירה ב-Go?
זה תלוי באופרנד. ל-slice או למערך: אינדקס ואיבר. למחרוזת: אינדקס של בייט ו-rune. ל-map: מפתח וערך. ל-channel: כל ערך שמתקבל. למספר שלם n (Go 1.22): מ-0 עד n פחות 1. לפונקציית iterator (Go 1.23): מה שהפונקציה מעבירה ל-yield. אפשר להשמיט את המשתנה השני, או לזרוק את הראשון עם _.
למה שינוי של ערך ה-range לא משנה את ה-slice שלי?
משתנה הערך הוא עותק של האיבר. for _, v := range items { v.Price = 0 } משנה רק את העותק. השתמשו באינדקס במקום זה: for i := range items { items[i].Price = 0 }, או עברו עם range על slice של pointers.
אפשר לעשות range על מספר שלם ב-Go?
כן, מאז Go 1.22: for i := range 5 { ... } רצה עם i מ-0 עד 4. for range 5 { ... } חוזרת חמש פעמים בלי משתנה. ב-go.mod של המודול חייב להיות כתוב go 1.22 ומעלה.
בטוח למחוק מ-map בזמן range עליו?
כן. מחיקה של רשומה שהלולאה עוד לא הגיעה אליה אומרת שהיא לא תופיע, ומחיקה של הרשומה הנוכחית היא בסדר. גם הוספת רשומות במהלך הלולאה מותרת, אבל רשומה חדשה עשויה להופיע ועשויה שלא, אז אל תסתמכו על זה.