Menu

Pointers ב-Golang: & ו-*, new, nil ומתי להשתמש בהם

pointer מחזיק את הכתובת של ערך. למדו על & ו-*, על new, על pointers ל-structs, למה בטוח להחזיר pointer למשתנה מקומי, מתי להשתמש ב-pointers, ועל ה-panic של גישה דרך pointer שהוא nil.

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

& ו-*

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 לזיכרון רציף.

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

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

להתחיל