Menu

range ב-Golang: כל הצורות, מה כל אחת מחזירה והמלכודות

מה for range מחזירה עבור slices, מערכים, מחרוזות, maps, channels, מספרים שלמים ופונקציות iterator, למה משתנה הערך הוא עותק, מה קורה כשמשנים slice או map בזמן range, ו-range על פונקציות ב-Go 1.23.

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

range היא הצורה של for שעוברת על אוסף. היא נותנת ערך אחד או שניים בכל איטרציה, ומה הערכים האלה תלוי בטיפוס שעליו עושים range:

אופרנדערך ראשוןערך שניהערות
slice []T, מערך [n]Tאינדקס intאיבר T, עותק
pointer למערך *[n]Tאינדקסאיבר
מחרוזתאינדקס של בייט intruneמפענחת 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)KVGo 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 עליו?

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

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

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

להתחיל