Menu

Interface ב-Golang: מימוש מרומז, any ומלכודת ה-nil

ב-Go מממשים interfaces באופן מרומז: כל טיפוס עם המתודות הנכונות מממש אותם. למדו על interfaces קטנים כמו io.Reader ו-fmt.Stringer, על ה-interface הריק any, על מלכודת ה-interface שהוא nil ועל הכלל לקבל interfaces ולהחזיר structs.

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

interface הוא קבוצה של מתודות

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

לא Rect ולא Circle מזכירים את Shape. זה מימוש מרומז, התכונה המגדירה של interfaces ב-Go. המשמעות היא שאפשר להגדיר בחבילה שלכם interface שטיפוסים מחבילות אחרות כבר מממשים, בלי לגעת בקוד שלהם.

interfaces קטנים מהספרייה הסטנדרטית

קוד Go מעדיף interfaces עם מתודה אחת או שתיים. החשובים ביותר:

Interfaceמתודהמשמש את
fmt.StringerString() stringההדפסה של fmt
errorError() stringכל פונקציה שיכולה להיכשל
io.ReaderRead(p []byte) (n int, err error)קבצים, רשת, gzip, bodies של HTTP
io.WriterWrite(p []byte) (n int, err error)קבצים, buffers, hashes, תשובות HTTP
sort.InterfaceLen, Less, Swapהחבילה sort
http.HandlerServeHTTP(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 נכשל עם הודעה שמציינת את המתודה החסרה.

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

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

להתחיל