Menu

Map ב-Golang: יצירה, בדיקת מפתחות, מעבר, מיון ומחיקה

maps ב-Go שומרים זוגות של מפתח וערך עם חיפוש מהיר. למדו איך יוצרים אותם, בודקים אם מפתח קיים עם comma-ok, מוחקים, עוברים עליהם (בסדר אקראי), ממיינים מפתחות, שומרים structs, ונמנעים מה-panic של map שהוא nil ושל כתיבות מקביליות.

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

יצירת map ושימוש בו

טיפוס map נכתב map[KeyType]ValueType. צרו אחד עם ליטרל או עם make, ואז קראו, כתבו ומחקו לפי מפתח.

פלט:

31
3
map[bob:26 cy:40]
2 0

שתי נוחויות מופיעות כאן. קריאה של מפתח חסר מחזירה את ערך האפס של טיפוס הערך (counts['z'] הוא 0), וזה מה שגורם לספירה עם m[k]++ לעבוד בלי שום הכנה. ו-fmt מדפיס maps עם מפתחות ממוינים, מה שנוח לדיבאג אבל לא אומר כלום על סדר המעבר.

make(map[K]V, n) מקבלת רמז גודל אופציונלי. היא מקצה מראש מקום לבערך n רשומות; בניגוד ל-slices, ל-map אין capacity שאפשר לקרוא בחזרה.

בדיקה אם מפתח קיים: comma-ok

מכיוון שמפתח חסר נקרא כערך האפס, m[k] == 0 לא יכול להבחין בין "לא קיים" ל"שמור כ-0". השתמשו בצורה עם שני הערכים:

המבנה if v, ok := m[k]; ok { ... } שומר את v ואת ok בתוך ה-scope של ה-if. זו אחת השורות הנפוצות ביותר בקוד Go.

מחיקת רשומות

delete(m, key) מסירה את הרשומה. מחיקה של מפתח שלא קיים לא עושה כלום, וכך גם מחיקה מ-map שהוא nil. כדי לרוקן map שלם, Go 1.21 הוסיפה את clear(m), ששומרת את ה-map שהוקצה כדי שאפשר יהיה להשתמש בו שוב.

מחיקת רשומות בזמן range על אותו map מותרת ובטוחה. רשומה שנמחקה לפני שהלולאה הגיעה אליה לא תופיע.

מעבר על map: הסדר אקראי

for k, v := range m מבקרת בכל רשומה פעם אחת, בסדר לא מוגדר. ה-runtime בוחר את נקודת ההתחלה באקראי בכוונה, כך ששתי לולאות על אותו map באותה תוכנית לעיתים קרובות לא מסכימות. הריצו את זה כמה פעמים:

כל קוד שהפלט שלו תלוי בסדר של map הוא באג שמחכה להרצה אחרת. בדיקות שמשוות פלט מודפס של מעבר על map הן הדוגמה הקלאסית.

מפתחות ממוינים

כדי לעבור על map לפי סדר המפתחות, קחו את המפתחות, מיינו אותם ופנו ל-map לפיהם. Go 1.23 הפכה את זה לשורה אחת עם iterators מהחבילות maps ו-slices:

ב-Go 1.22 ומטה, maps.Keys לא הייתה קיימת בספרייה הסטנדרטית. המקבילה היא לולאה:

keys := make([]string, 0, len(m))
for k := range m {
	keys = append(keys, k)
}
sort.Strings(keys)

עוד פונקציות עזר ב-maps: maps.Values, maps.Clone (העתקה רדודה), maps.Equal, maps.Copy(dst, src) ו-maps.DeleteFunc.

טיפוסי מפתח תקינים

מפתחות חייבים להיות ברי השוואה עם ==: מספרים, מחרוזות, בוליאנים, pointers, channels, מערכים של טיפוסים ברי השוואה, structs שכל השדות שלהם ברי השוואה, וערכי interface. slices, maps ופונקציות לא יכולים להיות מפתחות.

map[[]int]bool{}       // compile error: invalid map key type []int
map[[2]int]bool{}      // fine: arrays are comparable
map[struct{ X, Y int }]string{} // fine: a struct key for a grid position

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

מפתחות מסוג interface מתקמפלים גם כשהטיפוס הדינמי לא בר השוואה, ואז נכנסים ל-panic בזמן ריצה: שמירה של []int ב-map[any]int נכשלת עם runtime error: hash of unhashable type []int.

מפתחות של נקודה צפה עובדים, אבל NaN לא שווה לעצמו, כך שאפשר להכניס מפתח NaN שוב ושוב ואף פעם לא לקרוא אותו בחזרה. הימנעו ממפתחות float.

map של structs

map יכול להחזיק structs, אבל אי אפשר להשים ערך לשדה של struct ששמור ב-map, כי לערכים של map אין כתובת.

בחרו ערכים כשהרשומות קטנות ומוחלפות בשלמותן. בחרו pointers כשמעדכנים שדות לעיתים קרובות או משתפים את אותה רשומה מכמה מקומות. עם pointers, מפתח חסר מחזיר nil, ולכן ptrs["nope"].Score נכנס ל-panic.

maps של slices עובדים באותה דרך עם append: groups[k] = append(groups[k], v) לא צריך אתחול, כי מפתח חסר נותן slice שהוא nil ו-append מטפלת ב-nil.

maps מתנהגים כמו הפניות

ערך map מפנה לנתונים משותפים. השמה של map או העברה שלו לפונקציה לא מעתיקה את הרשומות: שני המשתנים רואים את אותו map.

זו הסיבה שפונקציה יכולה למלא map בלי להחזיר אותו, בניגוד ל-slice שהיא מוסיפה אליו.

ה-panic של nil map

ערך האפס של map הוא nil. map שהוא nil נקרא כמו map ריק, אבל כתיבה אליו נכנסת ל-panic.

פלט:

0 0
recovered: assignment to entry in nil map

המקרה של ה-struct הוא זה שתופס אנשים בפועל. אתחלו שדות map בבנאי (func NewCache() *Cache { return &Cache{data: map[string]string{}} }) או בעצלות, לפני הכתיבה הראשונה.

גישה מקבילית

maps לא בטוחים לשימוש מקבילי. אם goroutine אחת כותבת בזמן שאחרת קוראת או כותבת, ה-runtime עלול לעצור את התוכנית עם fatal error: concurrent map writes (או concurrent map read and map write). זו שגיאה פטאלית, לא panic, ולכן recover לא יכול לתפוס אותה.

הגנו על ה-map עם mutex:

זה תמיד מדפיס 50 50. השתמשו ב-sync.RWMutex כשיש הרבה יותר קריאות מכתיבות. sync.Map קיים לשני מקרים צרים (מפתחות שנכתבים פעם אחת ונקראים הרבה פעמים, או goroutines שעובדות על מפתחות נפרדים); לכל השאר mutex ו-map רגיל פשוטים יותר ובדרך כלל מהירים יותר. עוד על זה בעמוד על mutex.

טבלת עזר מהירה

פעולהקוד
יצירהm := map[string]int{} או make(map[string]int)
הוספה או עדכוןm[k] = v
קריאה (אפס אם חסר)v := m[k]
בדיקת קיוםv, ok := m[k]
מחיקהdelete(m, k)
הסרת הכולclear(m) (Go 1.21)
גודלlen(m)
מפתחות ממויניםslices.Sorted(maps.Keys(m)) (Go 1.23)
העתקהmaps.Clone(m)
השוואהmaps.Equal(a, b)

map עם ערכים מסוג struct{} הוא גם טיפוס ה-set של Go; ראו sets.

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

  • כתיבה ל-map שהוא nil. תמיד צרו אותו עם make, כולל שדות map בתוך structs.
  • הסתמכות על סדר המעבר. מיינו את המפתחות.
  • שימוש ב-m[k] != 0 כבדיקת קיום. השתמשו ב-comma-ok.
  • שינוי שדה של struct דרך m[k].Field. העתיקו החוצה וכתבו בחזרה, או שמרו pointers.
  • שיתוף map בין goroutines בלי נעילה. אי אפשר להתאושש מהקריסה.

שאלות נפוצות

איך בודקים אם מפתח קיים ב-map ב-Go?

השתמשו בצורה של החיפוש שמחזירה שני ערכים: v, ok := m[key]. ok הוא true כשהמפתח קיים ו-false כשהוא לא קיים, ואז v הוא ערך האפס. קריאה של m[key] לבד לא יכולה להבחין בין מפתח חסר למפתח ששמור עם ערך האפס.

למה סדר המעבר על map ב-Go אקראי?

השפה לא מגדירה סדר, וה-runtime מתחיל בכוונה כל range ממיקום אקראי, כדי שתוכניות לא יתחילו להסתמך על סדר מסוים. כדי לעבור לפי סדר המפתחות, אספו ומיינו את המפתחות: for _, k := range slices.Sorted(maps.Keys(m)) (Go 1.23).

איך מקבלים את כל המפתחות של map ב-Go?

מאז Go 1.23, maps.Keys(m) מחזירה iterator; הפכו אותו ל-slice עם slices.Collect(maps.Keys(m)), או ל-slice ממוין עם slices.Sorted(maps.Keys(m)). לפני 1.23, עברו בלולאה עם for k := range m והוסיפו כל מפתח ל-slice.

למה כתיבה ל-map נכנסת ל-panic עם "assignment to entry in nil map"?

משתנה ה-map הוצהר אבל אף פעם לא נוצר: var m map[string]int הוא nil. קריאה מ-map שהוא nil מחזירה ערכי אפס, אבל כתיבה נכנסת ל-panic. צרו אותו קודם עם m = make(map[string]int) או עם ליטרל m := map[string]int{}. שדה map בתוך struct צריך את אותו אתחול.

האם maps ב-Go בטוחים לשימוש מקבילי?

לא. כתיבות מקביליות, או כתיבה במקביל לקריאות, יכולות להקריס את התוכנית עם fatal error: concurrent map writes, ש-recover לא יכול לתפוס. הגנו על ה-map עם sync.Mutex או sync.RWMutex, או השתמשו ב-sync.Map למקרים הספציפיים שהוא תוכנן בשבילם.

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

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

להתחיל