Menu

Interfejsy w Golang: niejawna implementacja, any i pułapka nil

Interfejsy w Go są spełniane niejawnie: implementuje je każdy typ z odpowiednimi metodami. Poznaj małe interfejsy, takie jak io.Reader i fmt.Stringer, pusty interfejs any, pułapkę interfejsu nil i zasadę: przyjmuj interfejsy, zwracaj struktury.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

InterfejsMetodaUżywany przez
fmt.StringerString() stringwypisywanie przez fmt
errorError() stringkażdą funkcję, która może zawieść
io.ReaderRead(p []byte) (n int, err error)pliki, sieć, gzip, body HTTP
io.WriterWrite(p []byte) (n int, err error)pliki, bufory, funkcje skrótu, odpowiedzi HTTP
sort.InterfaceLen, Less, Swappakiet sort
http.HandlerServeHTTP(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.Open zwraca *os.File, a nie io.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.Reader prawie 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ę.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ