interface הוא קבוצה של מתודות
טיפוס interface מפרט חתימות של מתודות. כל טיפוס שיש לו את המתודות האלה מממש את ה-interface, בלי שום הצהרה שאומרת את זה.
לא Rect ולא Circle מזכירים את Shape. זה מימוש מרומז, התכונה המגדירה של interfaces ב-Go. המשמעות היא שאפשר להגדיר בחבילה שלכם interface שטיפוסים מחבילות אחרות כבר מממשים, בלי לגעת בקוד שלהם.
interfaces קטנים מהספרייה הסטנדרטית
קוד Go מעדיף interfaces עם מתודה אחת או שתיים. החשובים ביותר:
| Interface | מתודה | משמש את |
|---|---|---|
fmt.Stringer | String() string | ההדפסה של fmt |
error | Error() string | כל פונקציה שיכולה להיכשל |
io.Reader | Read(p []byte) (n int, err error) | קבצים, רשת, gzip, bodies של HTTP |
io.Writer | Write(p []byte) (n int, err error) | קבצים, buffers, hashes, תשובות HTTP |
sort.Interface | Len, Less, Swap | החבילה sort |
http.Handler | ServeHTTP(w, r) | net/http |
מכיוון של-io.Reader יש מתודה אחת, עשרות טיפוסים מממשים אותו, וכל פונקציה שמקבלת io.Reader עובדת עם כולם:
הפתגם של Go אומר "ככל שה-interface גדול יותר, ההפשטה חלשה יותר". interfaces גדולים יותר נבנים משילוב של קטנים: io.ReadWriter הוא Reader ועוד Writer, שנכתב על ידי הטמעה של interface אחד בתוך אחר.
any: ה-interface הריק
ל-interface{} אין מתודות, ולכן כל טיפוס מממש אותו. Go 1.18 הוסיפה את any ככינוי; הם זהים.
ערך any יכול להחזיק כל דבר, אבל כמעט אי אפשר לעשות איתו כלום עד שמשחזרים את הטיפוס הקונקרטי עם type assertion או type switch. העדיפו interface אמיתי או generics כשקבוצת הטיפוסים ידועה. any מתאים לנתונים דינמיים באמת, כמו JSON מפוענח במבנה לא ידוע, ולהדפסה.
מה ערך interface מכיל
ערך interface הוא זוג: טיפוס דינמי וערך דינמי. var s Shape = Rect{3, 4} שומר את הטיפוס Rect ועותק של הערך. קריאה ל-s.Area() מחפשת את המתודה של Rect בזמן ריצה.
interface הוא nil רק כששני החלקים ריקים. הכלל הזה גורם לבאג המבלבל ביותר ב-Go.
מלכודת ה-interface שהוא nil
pointer שהוא nil ונשמר בתוך interface יוצר interface שהוא לא nil.
פלט:
false
*main.MyError true
true
validate(true) מחזירה interface מסוג error שמחזיק את הטיפוס *MyError ואת הערך nil. ל-interface יש טיפוס, ולכן הוא לא שווה ל-nil, והענף if err != nil של הקוד הקורא רץ. קריאה ל-err.Error() שם הייתה נכנסת ל-panic בגישה לשדה של ה-receiver שהוא nil.
התיקון פשוט: הצהירו על המשתנה כ-error, לא כטיפוס ה-pointer הקונקרטי, או החזירו nil מפורש במסלול ההצלחה. לעולם אל תחזירו טיפוס pointer קונקרטי של שגיאה מפונקציה שהתוצאה שלה היא error. אותה מלכודת חלה על כל interface, לא רק על שגיאות.
לבדוק שטיפוס מממש interface
המימוש נבדק במקום שבו ערך מושם ל-interface. אם עדיין אין קוד שעושה את זה, טעות בחתימה של מתודה עוברת בלי שישימו לב. השמה ריקה ברמת החבילה הופכת את הבדיקה למפורשת:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
זה לא עולה כלום בזמן ריצה. אם ל-*LogWriter יש Write(p []byte) error במקום Write(p []byte) (int, error), ה-build נכשל:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
pointer receivers ו-interfaces
אם למתודה יש pointer receiver, רק לטיפוס ה-pointer יש את המתודה הזאת. *Counter מממש interface דרכה; Counter לא. הקומפיילר אומר Counter does not implement Incrementer (method Inc has pointer receiver). שמרו ב-interface את &Counter{}. העמוד על מתודות מסביר את ה-method sets.
לקבל interfaces, להחזיר structs
הנחיה נפוצה ב-Go: פונקציות צריכות לקבל פרמטרים מסוג interface ולהחזיר טיפוסים קונקרטיים.
- קבלת interface מאפשרת לקוד הקורא להעביר כל דבר שמתאים, כולל fakes לבדיקות. פונקציה שקוראת נתונים צריכה לקבל
io.Reader, לא*os.File. - החזרת טיפוס קונקרטי מאפשרת לקוד הקורא להשתמש בכל המתודות והשדות שלו, ומונעת את מלכודת ה-interface שהוא nil.
os.Openמחזירה*os.File, לאio.Reader.
הרגל קשור: הגדירו interfaces במקום שבו משתמשים בהם, לא במקום שבו מממשים אותם. אם ה-service שלכם צריך משהו שיכול לבצע Get(id) למשתמש, הצהירו על interface עם מתודה אחת בחבילה של ה-service, ותנו לחבילת מסד הנתונים פשוט לייצא את ה-struct שלה.
השוואת ערכי interface
שני ערכי interface שווים כשהטיפוסים הדינמיים שלהם זהים והערכים הדינמיים שלהם שווים. אם הטיפוס הדינמי לא בר השוואה (slice, map), == מתקמפל אבל נכנס ל-panic בזמן ריצה: runtime error: comparing uncomparable type []int.
טעויות נפוצות
- החזרת pointer מטיפוס מסוים שהוא nil בתור interface. החזירו
nilמפורש. - interfaces מוקדם מדי. כתבו קודם את הטיפוס הקונקרטי. הוסיפו interface כשמימוש שני או בדיקה צריכים אותו.
- pointer ל-interface.
*io.Readerכמעט אף פעם לא נכון. interface כבר מחזיק pointer כששומרים בו אחד. - interfaces גדולים. interfaces עם עשר מתודות קשים למימוש וקשים לזיוף בבדיקות. פצלו אותם.
שאלות נפוצות
איך מממשים interface ב-Go?
מגדירים על הטיפוס שלכם את המתודות שה-interface מפרט, עם אותם שמות ואותן חתימות. אין מילת מפתח implements. אם ל-*File יש Read(p []byte) (int, error), הוא io.Reader, אוטומטית. הקומפיילר בודק את זה בכל מקום שבו משימים את הערך לטיפוס ה-interface.
מה זה ה-interface הריק או any ב-Go?
ל-interface{} אין מתודות, ולכן כל טיפוס מממש אותו. מאז Go 1.18, any הוא כינוי מובנה ל-interface{}. ערך מסוג any יכול להחזיק כל דבר, אבל צריך type assertion או type switch כדי להוציא ממנו בחזרה טיפוס קונקרטי.
למה ה-interface שלי ב-Go לא nil כשהשמתי לו pointer שהוא nil?
ערך interface מחזיק טיפוס וערך. השמה של *MyError שהוא nil ל-error נותנת interface שהטיפוס שלו הוא *MyError והערך שלו nil, וה-interface הזה לא שווה ל-nil. כשאין שגיאה, החזירו nil מפורש במקום pointer מטיפוס מסוים שהוא nil.
איך בודקים בזמן קומפילציה שטיפוס מממש interface?
הוסיפו השמה ריקה ברמת החבילה: var _ io.Reader = (*MyReader)(nil). אם חסרה ל-*MyReader מתודה, ה-build נכשל עם הודעה שמציינת את המתודה החסרה.