slice בדוגמה אחת
slice הוא תצוגה על מערך שיכולה לגדול. יוצרים אותו עם ליטרל או עם make, מגדילים אותו עם append, וקוראים את הגודל שלו עם len.
ל-[]string אין אורך בתוך הסוגריים המרובעים. זה ההבדל מטיפוס מערך כמו [4]string, שהגודל שלו קבוע.
מה זה slice
ערך של slice הוא header קטן עם שלושה שדות:
- מצביע לאיבר במערך שמתחת
- אורך: כמה איברים ה-slice רואה
- קיבולת: כמה איברים קיימים מהמצביע הזה ועד סוף המערך שמתחת
העתקה של slice (הצבה שלו, העברה שלו לפונקציה) מעתיקה רק את ה-header הזה. שני העותקים מצביעים על אותם איברים. כמעט כל הפתעה עם slices נובעת משכחה של זה.
make: אורך וקיבולת
make([]T, len, cap) מקצה מערך שמתחת ומחזירה slice עליו. הקיבולת היא אופציונלית וברירת המחדל שלה היא האורך.
בלבול נפוץ: make([]int, 5) ואחריו חמש קריאות append נותנים עשרה איברים, שחמשת הראשונים מהם הם אפס. השתמשו ב-make([]int, 0, 5) כשמתכננים להוסיף עם append, או ב-make([]int, 5) כשמתכננים להציב לפי אינדקס.
הקצאה מראש של קיבולת כשהגודל הסופי ידוע חוסכת גדילות חוזרות. זו אופטימיזציה, לא דרישה.
append וגדילה
append מוסיפה איברים אחרי האורך הנוכחי. אם הקיבולת גדולה מספיק, היא כותבת במקום. אם לא, היא מקצה מערך גדול יותר, מעתיקה אליו את האיברים הישנים ומחזירה slice שמצביע על המערך החדש.
הקיבולת בערך מוכפלת כל עוד ה-slice קטן, וגדלה בפקטור קטן יותר (שמתקרב ל-1.25x) אחרי שהוא עובר 256 איברים. המספרים המדויקים הם פרט של סביבת הריצה שהשתנה בין גרסאות של Go, אז לעולם אל תכתבו קוד שתלוי בהם.
מכיוון ש-append עשויה להחזיר מערך אחר, חייבים להשתמש בתוצאה שלה:
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
הוספה של slice שלם משתמשת בתחביר הפריסה: s = append(s, other...).
חיתוך משתף את המערך שמתחת
s[low:high] יוצר header חדש של slice על אותו מערך, שמכסה את האינדקסים מ-low ועד high לא כולל. אפשר להשמיט כל אחד מהגבולות. שום איבר לא מועתק.
פלט:
[10 99 30 40 50]
3 4
השיתוף הזה הוא מה שהופך חיתוך לזול. הוא גם המקור לבאג המפורסם ביותר של slices.
באג ה-aliasing של append
כשלתת slice יש קיבולת פנויה, append אליו כותב לתוך המערך של ההורה, מעל איברים שההורה עדיין משתמש בהם.
פלט:
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a ו-b חולקים את המקום במערך שאחרי "cmd", ולכן ה-append השני דרס את הראשון. הקוד נראה תקין ועובד בכל פעם שהקיבולת במקרה מלאה, ולכן הבאג הזה מופיע רק לפעמים.
שני תיקונים:
- ביטוי החיתוך המלא
s[low:high:max]מגביל את הקיבולת ל-max-low. בלי קיבולת פנויה, ה-appendהבא חייב להקצות.first := base[:2:2]הופך את הדוגמה הראשונה לבטוחה. - העתיקו במפורש כש-slice יחיה יותר מהקריאה או שיוסיפו אליו באופן עצמאי:
slices.Clone(prefix)אוappend([]string(nil), prefix...).
מלכודת קרובה: slice קטן של מערך ענק שומר את כל המערך בחיים מבחינת ה-garbage collector. אם קוראים קובץ של 100 MB ושומרים את data[:10], ה-100 MB נשארים בזיכרון. שכפלו את החלק שאתם שומרים.
copy
הפונקציה המובנית copy(dst, src) מעתיקה min(len(dst), len(src)) איברים ומחזירה את המספר הזה. היא אף פעם לא מגדילה את dst.
copy מטפלת נכון במקור וביעד שחופפים, כך ש-copy(s[1:], s) מזיזה איברים קדימה בלי להשחית אותם.
החבילה slices
מאז Go 1.21 לחבילה הסטנדרטית slices יש פונקציות עזר גנריות לפעולות שפעם כתבו ביד.
| פונקציה | מה היא עושה |
|---|---|
Contains, Index | מציאת ערך (ContainsFunc, IndexFunc מקבלות פרדיקט) |
Sort, SortFunc, SortStableFunc | מיון במקום |
BinarySearch | חיפוש ב-slice ממוין |
Insert, Delete, DeleteFunc | הוספה או הסרה, מחזירות את ה-slice החדש |
Compact | הסרת כפילויות רצופות |
Equal, Compare | השוואה איבר אחרי איבר |
Clone, Reverse, Max, Min | מה שהשם אומר |
Collect, Sorted, Values, All | עבודה עם איטרטורים (Go 1.23) |
Delete ו-Insert מחזירות slice שחייבים להציב, כמו append. למיון יש דף משלו: מיון.
הסרת איברים תוך כדי לולאה לפי אינדקס מדלגת על איברים. השתמשו במקום זה ב-slices.DeleteFunc:
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
slices דו ממדיים
slice של slices נותן רשת שהשורות שלה יכולות להיות באורכים שונים. צריך להקצות כל שורה בנפרד.
שכחה של ה-make הפנימי משאירה כל שורה nil, ו-grid[1][2] = 7 גורם ל-panic עם index out of range [2] with length 0.
slices שהם nil ו-slices ריקים
slice שהוא nil מתנהג כמו slice ריק עבור len, cap, range ו-append, אז העדיפו var s []T כמצב ההתחלתי. אתחלו ל-[]T{} רק כשההבדל משנה, בעיקר בפלט JSON (null מול []). בדקו ריקנות עם len(s) == 0, לא עם s == nil.
slices ופונקציות
פונקציה שמקבלת slice יכולה לשנות את האיברים שלו, והקורא רואה את השינויים. היא לא יכולה לשנות את האורך אצל הקורא, כי היא קיבלה עותק של ה-header. פונקציה שמוסיפה איברים חייבת להחזיר את ה-slice החדש:
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
זו הסיבה ש-append, slices.Delete ו-slices.Insert כולן מחזירות slice.
טעויות נפוצות
- התעלמות מהתוצאה של
append. זה לא מתקמפל כשזורקים אותה לגמרי, אבלappend(s, x)שמוצב למשתנה אחר מזה שממשיכים להשתמש בו הוא באג לוגי. - שני appends מאותו בסיס. הם עשויים לחלוק אחסון. שכפלו את הבסיס או השתמשו בביטוי חיתוך מלא.
make([]T, n)ואחריוappend. משאיר n אפסים בהתחלה.- אינדקס מחוץ לטווח.
s[len(s)]גורם ל-panic. האיבר האחרון הואs[len(s)-1]. - השוואת slices עם
==. רקs == nilמתקמפל. השתמשו ב-slices.Equal.
שאלות נפוצות
מה ההבדל בין האורך לקיבולת של slice ב-Go?
len(s) הוא מספר האיברים שה-slice מחזיק כרגע. cap(s) הוא מספר האיברים שנכנסים במערך שמתחת, החל מהאיבר הראשון של ה-slice. append כותבת לתוך הקיבולת הפנויה כשיש כזאת, ומקצה מערך חדש וגדול יותר כשאין.
איך מוסיפים איברים ל-slice ב-Go?
קראו לפונקציה המובנית append והציבו את התוצאה בחזרה: s = append(s, x). היא יכולה להוסיף כמה ערכים (append(s, 1, 2, 3)) או slice אחר (append(s, other...)). תמיד השתמשו בערך המוחזר, כי append עשויה להחזיר slice שמצביע על מערך חדש.
איך מסירים איבר מ-slice ב-Go?
השתמשו ב-slices.Delete(s, i, i+1) (Go 1.21), שמזיזה את האיברים שאחריו אחורה ומחזירה את ה-slice הקצר יותר. מאז Go 1.22 היא גם מאפסת את המקומות שהתפנו בסוף. אם הסדר לא משנה, החליפו עם האיבר האחרון וקצרו: s[i] = s[len(s)-1]; s = s[:len(s)-1].
איך בודקים אם slice מכיל ערך ב-Go?
השתמשו ב-slices.Contains(s, v) מהחבילה הסטנדרטית slices (Go 1.21). slices.Index(s, v) מחזירה את המיקום או -1, ו-slices.ContainsFunc מקבלת פרדיקט. לחיפושים חוזרים בנתונים גדולים, בנו map במקום זה.
מה ההבדל בין slice שהוא nil ל-slice ריק ב-Go?
var s []int הוא nil; s := []int{} ריק אבל לא nil. לשניהם אורך 0, ושניהם עובדים עם len, range ו-append. ההבדל מופיע בבדיקות s == nil וב-encoding/json, שמקודדת slice שהוא nil כ-null ו-slice ריק כ-[].