Interfejs to zbiór metod
Typ interfejsu wymienia sygnatury metod. Każdy typ, który ma te metody, spełnia interfejs, bez żadnej deklaracji, która by to ogłaszała.
Ani Rect, ani Circle nie wspomina o Shape. To niejawna implementacja, cecha definiująca interfejsy w Go. Oznacza, że możesz zdefiniować w swoim pakiecie interfejs, który typy z innych pakietów już spełniają, bez ruszania ich kodu.
Małe interfejsy z biblioteki standardowej
Kod w Go preferuje interfejsy z jedną lub dwiema metodami. Najważniejsze z nich:
| Interfejs | Metoda | Używany przez |
|---|---|---|
fmt.Stringer | String() string | wypisywanie przez fmt |
error | Error() string | każdą funkcję, która może zawieść |
io.Reader | Read(p []byte) (n int, err error) | pliki, sieć, gzip, body HTTP |
io.Writer | Write(p []byte) (n int, err error) | pliki, bufory, funkcje skrótu, odpowiedzi HTTP |
sort.Interface | Len, Less, Swap | pakiet sort |
http.Handler | ServeHTTP(w, r) | net/http |
Ponieważ io.Reader ma jedną metodę, implementują go dziesiątki typów, a każda funkcja przyjmująca io.Reader działa z nimi wszystkimi:
Przysłowie Go mówi: „im większy interfejs, tym słabsza abstrakcja”. Większe interfejsy buduje się, łącząc małe: io.ReadWriter to Reader plus Writer, zapisany przez osadzenie jednego interfejsu w drugim.
any: pusty interfejs
interface{} nie ma metod, więc spełnia go każdy typ. Go 1.18 dodało any jako alias; oba są identyczne.
Wartość any może przechowywać cokolwiek, ale niewiele da się z nią zrobić, dopóki nie odzyskasz konkretnego typu przez asercję typu albo type switch. Gdy zbiór typów jest znany, wybierz prawdziwy interfejs albo generyki. any pasuje do naprawdę dynamicznych danych, takich jak zdekodowany JSON o nieznanym kształcie, i do wypisywania.
Co zawiera wartość interfejsu
Wartość interfejsu to para: typ dynamiczny i wartość dynamiczna. var s Shape = Rect{3, 4} przechowuje typ Rect i kopię wartości. Wywołanie s.Area() wyszukuje metodę Rect w czasie działania.
Interfejs jest nil tylko wtedy, gdy obie części są puste. Ta reguła powoduje najbardziej mylący błąd w Go.
Pułapka interfejsu nil
Wskaźnik nil zapisany w interfejsie daje interfejs, który nie jest nil.
Wynik:
false
*main.MyError true
true
validate(true) zwraca interfejs error z typem *MyError i wartością nil. Interfejs ma typ, więc nie jest równy nil, i wykonuje się gałąź if err != nil w kodzie wywołującym. Wywołanie tam err.Error() skończyłoby się paniką przy dostępie do pola nilowego odbiorcy.
Poprawka jest prosta: zadeklaruj zmienną jako error, a nie jako konkretny typ wskaźnikowy, albo zwracaj dosłowne nil na ścieżce sukcesu. Nigdy nie zwracaj konkretnego typu wskaźnika na błąd z funkcji, której wynikiem jest error. Ta sama pułapka dotyczy każdego interfejsu, nie tylko błędów.
Sprawdzanie, czy typ implementuje interfejs
Implementacja jest sprawdzana tam, gdzie wartość zostaje przypisana do interfejsu. Jeśli żaden kod jeszcze tego nie robi, pomyłka w sygnaturze metody przejdzie niezauważona. Puste przypisanie na poziomie pakietu sprawia, że sprawdzenie jest jawne:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
W czasie działania nic to nie kosztuje. Jeśli *LogWriter ma Write(p []byte) error zamiast Write(p []byte) (int, error), budowanie się nie powiedzie:
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)
Odbiorcy wskaźnikowi a interfejsy
Jeśli metoda ma odbiorcę wskaźnikowego, tę metodę ma tylko typ wskaźnikowy. *Counter spełnia dzięki niej interfejs, a Counter nie. Kompilator zgłasza Counter does not implement Incrementer (method Inc has pointer receiver). Zapisz w interfejsie &Counter{}. Strona o metodach wyjaśnia zbiory metod.
Przyjmuj interfejsy, zwracaj struktury
Popularna wskazówka w Go: funkcje powinny przyjmować parametry będące interfejsami i zwracać konkretne typy.
- Przyjmowanie interfejsu pozwala wywołującym przekazać wszystko, co pasuje, łącznie z atrapami w testach. Funkcja, która czyta dane, powinna przyjmować
io.Reader, a nie*os.File. - Zwracanie konkretnego typu pozwala wywołującym używać wszystkich jego metod i pól oraz omija pułapkę interfejsu nil.
os.Openzwraca*os.File, a nieio.Reader.
Powiązany nawyk: definiuj interfejsy tam, gdzie są używane, a nie tam, gdzie są implementowane. Jeśli twój serwis potrzebuje czegoś, co potrafi wykonać Get(id) dla użytkownika, zadeklaruj jednometodowy interfejs w pakiecie serwisu, a pakiet bazy danych niech po prostu eksportuje swoją strukturę.
Porównywanie wartości interfejsów
Dwie wartości interfejsu są równe, gdy ich typy dynamiczne są identyczne, a wartości dynamiczne równe. Jeśli typ dynamiczny nie jest porównywalny (slice, mapa), == się kompiluje, ale w czasie działania wywołuje panikę: runtime error: comparing uncomparable type []int.
Częste błędy
- Zwracanie typowanego wskaźnika nil jako interfejsu. Zwracaj dosłowne
nil. - Interfejsy za wcześnie. Najpierw napisz konkretny typ. Dodaj interfejs, gdy potrzebuje go druga implementacja albo test.
- Wskaźnik na interfejs.
*io.Readerprawie nigdy nie jest właściwy. Interfejs już przechowuje wskaźnik, gdy go w nim zapiszesz. - Duże interfejsy. Interfejsy z dziesięcioma metodami trudno zaimplementować i trudno podrobić w testach. Podziel je.
Najczęściej zadawane pytania
Jak zaimplementować interfejs w Go?
Zdefiniuj w swoim typie metody, które wymienia interfejs, z tymi samymi nazwami i sygnaturami. Nie ma słowa kluczowego implements. Jeśli *File ma Read(p []byte) (int, error), to automatycznie jest io.Reader. Kompilator sprawdza to w każdym miejscu, gdzie przypisujesz wartość do typu interfejsu.
Czym jest pusty interfejs lub any w Go?
interface{} nie ma metod, więc spełnia go każdy typ. Od Go 1.18 any to wbudowany alias interface{}. Wartość typu any może przechowywać cokolwiek, ale aby odzyskać konkretny typ, potrzebujesz asercji typu albo type switcha.
Dlaczego mój interfejs w Go nie jest nil, choć przypisano do niego wskaźnik nil?
Wartość interfejsu przechowuje typ i wartość. Przypisanie nilowego *MyError do error daje interfejs, którego typem jest *MyError, a wartością nil, i taki interfejs nie jest równy nil. Gdy nie ma błędu, zwracaj dosłowne nil zamiast typowanego wskaźnika nil.
Jak sprawdzić w czasie kompilacji, że typ implementuje interfejs?
Dodaj puste przypisanie na poziomie pakietu: var _ io.Reader = (*MyReader)(nil). Jeśli *MyReader nie ma którejś metody, budowanie się nie powiedzie, a komunikat poda brakującą metodę.