Menu

Embed Struct ב-Golang: הטמעה ומתודות מקודמות

הטמעה (embedding) שמה טיפוס אחד בתוך אחר בלי שם שדה, כך שהשדות והמתודות שלו מקודמים לטיפוס החיצוני. למדו איך קידום עובד, הטמעת interfaces, התנגשויות שמות, ולמה הטמעה היא לא ירושה.

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

הטמעה בדוגמה אחת

שדה עם טיפוס אבל בלי שם הוא שדה מוטמע. השדות והמתודות של הטיפוס המוטמע נגישים ישירות על הטיפוס החיצוני.

פלט:

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

השם של השדה המוטמע הוא שם הטיפוס שלו, User. כך פונים אליו בליטרלים (User: User{...}) וכשצריך את הערך הפנימי עצמו (a.User). אי אפשר להציב שדה מקודם לפי השם הקצר שלו בליטרל: Admin{Name: "Ana"} נכשל עם unknown field Name in struct literal of type Admin.

מתודות מקודמות מממשות interfaces

קידום מוסיף את המתודות של הטיפוס המוטמע ל-method set של הטיפוס החיצוני. לכן הטיפוס החיצוני מממש כל interface שהטיפוס הפנימי מממש.

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

להטמעת מצביע יש מחיר: המצביע יכול להיות nil. Service{} בלי Logger מתקמפל, ואז svc.Log(...) גורם ל-panic.

הטמעה היא לא ירושה

הטמעה נראית כמו יצירת תת מחלקה, אבל שלושה דברים מתנהגים אחרת מאשר ב-Java או ב-Python.

הטיפוס החיצוני הוא לא הטיפוס הפנימי. Admin הוא לא User. פונקציה שמקבלת User לא תקבל Admin; העבירו את a.User.

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

פלט:

woof
The animal says ...

בשפה מבוססת מחלקות, Speak הייתה מדפיסה "woof". ב-Go, d.Speak() הוא קיצור של d.Animal.Speak(), וה-receiver של הקריאה הזאת הוא ה-Animal. כשרוצים התנהגות שמשתנה לפי טיפוס, השתמשו ב-interface: העבירו Sounder לקוד שצריך אותו.

הטיפוס הפנימי לא יודע שהוא מוטמע. אין super. כדי להרחיב מתודה מקודמת, הגדירו מתודה באותו שם על הטיפוס החיצוני וקראו לפנימית במפורש:

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

התנגשויות שמות והסתרה

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

כששני טיפוסים מוטמעים באותו עומק מקדמים את אותו שם, השם הזה נעשה דו משמעי. התוכנית עדיין מתקמפלת כל עוד שום דבר לא משתמש בשם הדו משמעי:

פתרו את ההתנגשות בעזרת הנתיב המלא, או בהגדרת ID על הטיפוס החיצוני.

הטמעת interfaces

interfaces יכולים להטמיע interfaces אחרים. הספרייה הסטנדרטית בונה כך interfaces גדולים יותר:

type ReadWriter interface {
	Reader
	Writer
}

גם struct יכול להטמיע interface. ה-struct אז מממש את ה-interface הזה דרך כל ערך ששומרים בשדה, ודורסים רק את המתודות שמעניינות אתכם. זה דפוס ה-decorator כמעט בלי קוד:

ה-c.Reader.Read(p) המפורש הכרחי. כתיבה של c.Read(p) בתוך Read הייתה קוראת לעצמה לנצח.

הטמעת interface ב-test fake היא קיצור דרך נפוץ: מטמיעים את ה-interface הגדול, מממשים את המתודה היחידה שהבדיקה צריכה, ומשאירים את השאר. כל מתודה שלא מומשה גורמת ל-panic עם nil pointer dereference אם קוראים לה, ולעיתים קרובות זה בדיוק מה שרוצים בבדיקה.

שימוש אמיתי נפוץ: הטמעת sync.Mutex

type Stats struct {
	sync.Mutex
	hits map[string]int
}

func (s *Stats) Hit(page string) {
	s.Lock()
	defer s.Unlock()
	s.hits[page]++
}

זה נקרא יפה, אבל זה גם מייצא את Lock ואת Unlock כחלק מה-API של Stats, כך שכל קורא יכול לנעול את ה-struct שלכם. בטיפוסים שמשמשים מחוץ לחבילה, שדה עם שם (mu sync.Mutex) שומר את הנעילה פרטית. השיקול הזה חל על כל הטמעה: כל מה שהטיפוס המוטמע מייצא הופך לחלק מהממשק הציבורי של הטיפוס שלכם.

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

  • ציפייה ל-virtual dispatch. מתודות מקודמות אף פעם לא קוראות לדריסות של הטיפוס החיצוני.
  • הצבת שדות מקודמים בליטרל. השתמשו בשם של הטיפוס המוטמע: Admin{User: User{Name: "Ana"}}.
  • מצביעים מוטמעים שהם nil. צריך להציב *T או interface מוטמעים לפני שקוראים למתודות שלהם.
  • חשיפת API בטעות. הטמעה מייצאת על הטיפוס שלכם את כל המתודות המיוצאות של הטיפוס הפנימי.

שאלות נפוצות

מה זה struct embedding ב-Go?

הצהרה על שדה עם טיפוס בלבד ובלי שם: type Admin struct { User; Level int }. השדות והמתודות של ה-User המוטמע מקודמים, כך ש-a.Name ו-a.Greet() עובדים ישירות על Admin. הערך המוטמע הוא עדיין שדה רגיל, שאפשר להגיע אליו כ-a.User.

האם יש ירושה ב-Go?

לא. ל-Go אין מחלקות ואין ירושה של תת טיפוסים. הטמעה נותנת שימוש חוזר בקוד דרך הרכבה: הטיפוס החיצוני מקבל את המתודות של הטיפוס הפנימי, אבל Admin הוא לא User. אי אפשר להעביר Admin במקום שבו מצופה User, ומתודות מקודמות לא יכולות לקרוא בחזרה למתודות של הטיפוס החיצוני. פולימורפיזם ב-Go מגיע מ-interfaces.

איך מאתחלים struct מוטמע ב-Go?

בליטרל מורכב, קראו לשדה המוטמע בשם הטיפוס שלו: Admin{User: User{Name: "Ana"}, Level: 2}. אי אפשר להציב שדות מקודמים ישירות בליטרל: Admin{Name: "Ana"} נכשל עם unknown field Name in struct literal of type Admin.

אפשר להטמיע interface בתוך struct ב-Go?

כן. ה-struct אז מממש את ה-interface דרך הערך המוטמע, ואפשר לדרוס מתודות בודדות. זה נפוץ ב-decorators וב-test fakes. אם שדה ה-interface המוטמע הוא nil, קריאה למתודה שלא דרסתם גורמת ל-panic עם nil pointer dereference.

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

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

להתחיל