Menu

מתודות ב-Golang: value receiver מול pointer receiver

מתודה היא פונקציה עם receiver. למדו איך מצהירים על מתודות, מתי להשתמש ב-value receiver ומתי ב-pointer receiver, מה הם method sets ואיך הם קשורים ל-interfaces, ואיך מגדירים מתודות על טיפוסים שהם לא structs.

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

הצהרה על מתודה

מתודה היא פונקציה עם receiver: פרמטר נוסף שנכתב לפני שם המתודה. ה-receiver קושר את המתודה לטיפוס.

שם ה-receiver הוא בדרך כלל אות אחת או שתיים מתוך שם הטיפוס (c בשביל Circle). ב-Go לא משתמשים ב-this או ב-self, ומדריכי הסגנון ממליצים להימנע מהשמות האלה.

מכיוון שלטיפוס Circle יש מתודה String() string, הוא מממש את fmt.Stringer, ו-fmt.Println מדפיסה את התוצאה במקום {2}.

value receivers ו-pointer receivers

value receiver (c Circle) מקבל עותק. pointer receiver (c *Circle) מקבל את הכתובת, כך ששינויים בשדות נשמרים.

IncByValue מתקמפלת ולא עושה שום דבר שאפשר לראות, באג קלאסי. אם מתודה אמורה לשנות את ה-receiver שלה, היא צריכה pointer receiver.

הקריאות מראות גם את שתי הנוחויות ש-Go נותנת לכם. c.Inc() על משתנה שיש לו כתובת נכתב מחדש כ-(&c).Inc(), ו-p.Value() על pointer נכתב מחדש כ-(*p).Value(). כמעט אף פעם לא כותבים & או * רק כדי לקרוא למתודה.

ה-& האוטומטי צריך משהו שיש לו כתובת. לאיבר של map או לליטרל מורכב אין:

m := map[string]Counter{"a": {}}
m["a"].Inc()    // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable

שמרו pointers ב-map (map[string]*Counter), או העתיקו את הערך החוצה, שנו אותו וכתבו אותו בחזרה.

איזה receiver לבחור

השתמשו ב-pointer receiver כשהשתמשו ב-value receiver כש
המתודה משנה את ה-receiverהטיפוס קטן והמתודות שלו אף פעם לא משנות אותו (time.Time, נקודה)
ה-struct גדול (העתקה עולה)הטיפוס הוא map, func או chan (כבר הפניה)
הטיפוס מחזיק sync.Mutex או משהו דומהאתם רוצים שהערך יתנהג כמו טיפוס פרימיטיבי, שבטוח להעתיק
מתודות אחרות של הטיפוס כבר משתמשות ב-pointers

ה-FAQ של Go והספרייה הסטנדרטית פועלים לפי כלל אצבע אחד: היו עקביים. אם מתודה כלשהי צריכה pointer receiver, תנו לכל המתודות pointer receivers, כך שה-method set יהיה צפוי. go vet מסמן struct שמחזיק sync.Mutex ומועתק דרך value receiver (passes lock by value).

method sets ו-interfaces

ההבחנה חשובה במיוחד ל-interfaces. ה-method set של T מכיל את המתודות עם value receiver. ה-method set של *T מכיל גם את המתודות עם value receiver וגם את אלה עם pointer receiver.

כתיבת Square{Side: 3} בלי ה-& ב-slice הזה לא מתקמפלת:

cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)

הסיבה: interface יכול להחזיק עותק של Square שאין לו כתובת, ולכן Go לא יכולה לקחת את הכתובת שלו כדי לקרוא למתודה עם pointer. ראו את העמוד על interfaces להמשך הסיפור.

מתודות על טיפוסים שהם לא structs

לכל טיפוס בעל שם שהוצהר בחבילה שלכם יכולות להיות מתודות, לא רק ל-structs.

המגבלות: הטיפוס חייב להיות מוגדר באותה חבילה כמו המתודה, והטיפוס הבסיסי שלו לא יכול להיות pointer או interface. אי אפשר להוסיף מתודות ל-int, ל-string או ל-time.Time מהחבילה שלכם. עטיפה בטיפוס חדש בעל שם היא הדרך של Go. String() על טיפוס של מספר שלם הוא גם הדרך שבה enums מקבלים פלט קריא; ראו enums עם iota.

method values ו-method expressions

מתודה שקשורה ל-receiver מסוים היא ערך של פונקציה:

method value עם value receiver מעתיק את ה-receiver כשהוא נוצר, ולכן f עדיין אומרת Ana. עם pointer receiver הוא היה שומר את ה-pointer ורואה שינויים מאוחרים יותר. method values נוחים ל-callbacks: http.HandleFunc("/", srv.handleIndex).

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

  • value receiver על מתודה שמשנה. מתקמפל, ומשנה בשקט עותק.
  • ערבוב של סוגי receivers. כך לא ברור אם T או *T מממש interface. בחרו סוג אחד לכל טיפוס.
  • קריאה למתודה עם pointer receiver על pointer שהוא nil. מותר, והמתודה רצה עם receiver שהוא nil. היא נכנסת ל-panic רק כשהיא נוגעת בשדה. חלק מהטיפוסים (כמו רשימה מקושרת שבטוחה ל-nil) משתמשים בזה בכוונה; רובם לא מצפים לזה.

שאלות נפוצות

מה ההבדל בין מתודה לפונקציה ב-Go?

למתודה יש receiver, שנכתב בין func לשם: func (c Circle) Area() float64. קוראים לה על ערך (c.Area()), היא שייכת ל-method set של הטיפוס, ובאמצעותה טיפוס מממש interfaces. חוץ מזה היא מתנהגת כמו פונקציה שהארגומנט הראשון שלה הוא ה-receiver.

מתי להשתמש ב-pointer receiver ב-Go?

השתמשו ב-pointer receiver כשהמתודה צריכה לשנות את ה-receiver, כשה-struct גדול מספיק כדי שהעתקה שלו בכל קריאה תהיה בזבוז, או כשהטיפוס מכיל משהו שאסור להעתיק, כמו sync.Mutex. אם מתודה כלשהי של טיפוס צריכה pointer receiver, ההמלצה המקובלת היא לתת לכל המתודות שלו pointer receivers, לשם עקביות.

למה הטיפוס שלי לא מממש את ה-interface כשלמתודה יש pointer receiver?

מתודות עם pointer receiver שייכות ל-method set של *T, לא של T. לכן var s Shape = Square{} נכשל עם Square does not implement Shape (method Area has pointer receiver). השימו pointer במקום: var s Shape = &Square{}.

אפשר להגדיר מתודות על טיפוסים מובנים ב-Go?

לא ישירות. אפשר להצהיר על מתודות רק על טיפוסים בעלי שם שהוגדרו באותה חבילה. הגדירו קודם טיפוס משלכם, type Celsius float64, ואז הוסיפו לו מתודות. אי אפשר להוסיף מתודות ל-int, ל-string או לטיפוס מחבילה אחרת.

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

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

להתחיל