& ו-*
pointer מחזיק את כתובת הזיכרון של ערך. &x נותן לכם pointer ל-x. *p הולך אחרי ה-pointer אל הערך.
לכוכבית יש שני תפקידים. בתוך טיפוס (*int) היא אומרת "pointer ל-int". בתוך ביטוי (*p) היא אומרת "הערך ש-p מצביע עליו". הדפסה של p עצמו מראה כתובת כמו 0xc000012345, שמשתנה בין הרצות.
למה pointers קיימים: שיתוף במקום העתקה
Go מעבירה הכול לפי ערך. פונקציה מקבלת עותק של כל ארגומנט, ולכן היא לא יכולה לשנות את המשתנה של הקוד הקורא. העברת pointer מעתיקה במקום זה את הכתובת, ושני הצדדים מגיעים לאותו ערך.
ב-Go אין references במובן של C++. "העברה לפי reference" ב-Go פירושה העברה של pointer לפי ערך.
Pointers ל-structs
pointers ל-structs הם ה-pointers הנפוצים ביותר בקוד Go. גישה לשדה מבצעת dereference אוטומטית: p.Name פירושו (*p).Name.
מתודות פועלות לפי אותו היגיון: pointer receiver מאפשר למתודה לשנות את ה-struct. העמוד על מתודות מסביר את הבחירה בין value receiver ל-pointer receiver.
new
new(T) מקצה T מאופס ומחזירה את הכתובת שלו. זה אותו דבר כמו להצהיר על משתנה ולקחת את הכתובת שלו.
p := new(int) // *int pointing at 0
var x int
q := &x // same thing, two lines
a := new(Account) // same as &Account{}
בפועל &T{...} נפוץ יותר ל-structs, כי הוא מאפשר לקבוע שדות באותו ביטוי. new נוחה ל-pointers לטיפוסים בסיסיים. אל תבלבלו אותה עם make, שבונה slices, maps ו-channels ומחזירה אותם מוכנים לשימוש, לא כ-pointers.
בטוח להחזיר pointer למשתנה מקומי
ב-C, החזרת הכתובת של משתנה מקומי היא באג. ב-Go זה קוד רגיל.
ה-escape analysis של הקומפיילר מחליט אם ערך יחיה ב-stack או ב-heap. אתם אף פעם לא בוחרים, ואף פעם לא משחררים זיכרון: ה-garbage collector מחזיר אותו כששום דבר לא מצביע עליו. go build -gcflags=-m מדפיס את ההחלטות האלה, אם זה מסקרן אתכם.
pointers שהם nil
ערך האפס של כל טיפוס pointer הוא nil. dereference ל-nil נכנס ל-panic.
פלט:
1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference
בלי ה-recover, התוכנית הייתה קורסת עם ההודעה הזאת ועם stack trace שמתחיל ב-panic: runtime error: invalid memory address or nil pointer dereference וב-[signal SIGSEGV: segmentation violation ...]. המקורות הרגילים:
- משתנה pointer או שדה ב-struct שאף פעם לא קיבלו ערך
m[key]עלmap[string]*Tעם מפתח חסר, שמחזירnil- התעלמות משגיאה:
f, _ := os.Open(path)משאיר אתfכ-nil בכישלון - מתודה עם pointer receiver שנקראת על pointer שהוא nil, ואז קוראת שדה
קריאה למתודה על pointer שהוא nil היא כשלעצמה חוקית. ה-panic קורה רק כשהמתודה נוגעת בשדות של ה-receiver.
מתי להשתמש ב-pointer
השתמשו ב-pointer כש:
- פונקציה או מתודה צריכה לשנות את הערך של הקוד הקורא
- הערך הוא struct גדול שמועבר הרבה
- אסור להעתיק את הטיפוס (הוא מכיל
sync.Mutex, או מייצג משאב ייחודי כמו חיבור) - צריך לבטא "אין ערך" עם
nil, למשל שדה אופציונלי ב-struct של JSON (*intמבחין בין "חסר" ל-0)
אל תשתמשו ב-pointer כש:
- הערך קטן ולקריאה בלבד (
time.Time,Point) - הטיפוס כבר מתנהג כמו הפניה: slices, maps, channels, פונקציות ו-interfaces כמעט אף פעם לא צריכים
*לפניהם.*[]intאו*map[string]intהם כמעט תמיד טעות
pointers לא מהירים יותר אוטומטית. העתקה של struct קטן זולה, ו-pointer יכול לכפות הקצאה ב-heap ולהוסיף עבודה ל-garbage collector.
pointers ומשתני לולאה
לקחת את הכתובת של משתנה לולאה זה בטוח מאז Go 1.22, כי לכל איטרציה יש משתנה משלה:
זה מדפיס 0 1 2. לפני Go 1.22 (שגם הציגה את range 3), אותה לולאה שנכתבה כ-for i := 0; i < 3; i++ הדפיסה 3 3 3, כי שלושת ה-pointers חלקו משתנה אחד. שימו לב ש-&v ב-for _, v := range items מצביע על עותק של האיבר, לא על האיבר שב-slice. כדי לקבל pointer לאיבר עצמו, השתמשו ב-&items[i].
טעויות נפוצות
- dereference בלי בדיקה. בדקו
if p == nilבכל מקום שבו nil אפשרי. - pointer לערך של range.
&vמצביע על עותק; השתמשו ב-&s[i]. - pointers ל-slices ול-maps. כמעט אף פעם לא נחוצים. החזירו במקום זה את ה-slice החדש.
- החזקת
&s[i]לאורךappend. אםappendמקצה מחדש, ה-pointer עדיין מפנה למערך הישן, וכתיבות מאוחרות יותר הולכות למקום שלא רואים.
שאלות נפוצות
מה המשמעות של & ו-* ב-Go?
&x לוקח את הכתובת של x ונותן pointer מטיפוס *T. *p מבצע dereference ל-pointer: הוא קורא או כותב את הערך שה-pointer מצביע עליו. בתוך טיפוס, *T פירושו "pointer ל-T".
מה ההבדל בין new ל-make ב-Go?
new(T) מקצה T מאופס ומחזירה *T; היא עובדת עם כל טיפוס. make עובדת רק עם slices, maps ו-channels, ומחזירה ערך מאותחל (לא מאופס, ולא pointer) של הטיפוס הזה: make(map[string]int) מוכן לשימוש, בעוד ש-new(map[string]int) הוא pointer ל-map שהוא nil.
אפשר להחזיר pointer למשתנה מקומי ב-Go?
כן. בניגוד ל-C, זה בטוח. ה-escape analysis של הקומפיילר רואה שהמשתנה חי יותר זמן מהפונקציה ומקצה אותו ב-heap. func newInt() *int { x := 5; return &x } הוא קוד Go תקין.
מה גורם ל-"invalid memory address or nil pointer dereference" ב-Go?
קריאה או כתיבה דרך pointer שהוא nil: משתנה *T שאף פעם לא קיבל ערך, חיפוש של מפתח חסר ב-map[K]*V, או פונקציה שהחזירה nil יחד עם שגיאה שלא בדקתם. בדקו אם הוא nil, או אתחלו את ה-pointer לפני השימוש.
יש ב-Go אריתמטיקה של pointers?
לא. אי אפשר להוסיף ל-pointer או לגשת לזיכרון דרכו לפי אינדקס. החבילה unsafe מאפשרת את זה לקוד low-level, בלי שום הבטחות בטיחות. קוד Go רגיל משתמש ב-slices לזיכרון רציף.