Menu

Błędy w Go (Golang): wrap, errors.Is, errors.As i własne typy

Definiuj błędy sentinel i własne typy błędów, opakowuj błędy przez %w, sprawdzaj je przez errors.Is i errors.As, łącz kilka przez errors.Join i pisz metody Unwrap oraz Is, gdy ich potrzebujesz.

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

Trzy rodzaje błędów

Kod w Go używa trzech postaci błędów, od najprostszej do najbogatszej:

  1. Błąd doraźny: errors.New("...") lub fmt.Errorf("...") utworzony na miejscu. Wywołujący mogą tylko przeczytać komunikat.
  2. Błąd sentinel: zmienna na poziomie pakietu, taka jak io.EOF. Wywołujący mogą ją sprawdzić przez errors.Is.
  3. Typ błędu: struktura z metodą Error(), zawierająca pola. Wywołujący mogą ją wyciągnąć przez errors.As i odczytać pola.

Wybierz najprostszą postać, która pozwala wywołującym zrobić to, czego potrzebują.

Błędy sentinel

Sentinel to wartość błędu zadeklarowana raz i porównywana przez tożsamość. Zgodnie z konwencją nazwy zaczynają się od Err.

Wynik:

mug bought
cap is out of stock, notify me later
buy "hat": not found

Dwa wywołania errors.New z tym samym tekstem dają różne błędy: errors.New("x") == errors.New("x") to false. Dlatego sentinel musi być jedną wspólną zmienną.

Sentinele stają się częścią API twojego pakietu. Gdy wywołujący zaczną sprawdzać ErrNotFound, nie możesz przestać go zwracać, nie psując im kodu. Eksportuj tylko te, od których wywołujący naprawdę muszą uzależniać dalsze działanie.

Własne typy błędów

Gdy wywołujący potrzebuje szczegółów (które pole, jaki kod statusu, jakie opóźnienie przed ponowieniem), zdefiniuj typ.

Wynik:

register: age: must be between 0 and 150
bad field: age

Metoda ma odbiorcę wskaźnikowego, a funkcja zwraca &ValidationError{...}, więc celem dla errors.As jest *ValidationError i przekazujesz jego adres (&ve, czyli **ValidationError). Pomylenie tego poziomu to klasyczny błąd przy errors.As: przy odbiorcy wartościowym funkcja zwracałaby ValidationError{...}, a zmienna miałaby postać var ve ValidationError. go vet wyłapuje najczęstszą pomyłkę, czyli przekazanie ve zamiast &ve: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.

Opakowywanie przez %w

fmt.Errorf z %w zwraca błąd, który pamięta opakowany błąd. Każda warstwa dodaje kontekst i zachowuje łańcuch.

Wynik:

start: connect db: timeout
  *fmt.wrapError: start: connect db: timeout
  *fmt.wrapError: connect db: timeout
  *errors.errorString: timeout
true
false

Z %v komunikat jest taki sam, ale łańcuch zostaje przerwany, więc errors.Is zwraca false. Wybierz %w, gdy wywołujący mają widzieć przyczynę, a %v, gdy przyczyna to szczegół implementacji, od którego nie chcesz, żeby zależeli.

Od Go 1.20 jedno wywołanie może opakować kilka błędów: fmt.Errorf("%w; %w", err1, err2). errors.Is pasuje wtedy do każdego z nich.

errors.Join: kilka błędów naraz

Walidacja i sprzątanie często dają więcej niż jeden błąd. errors.Join (Go 1.20) łączy je w paczkę.

Wynik:

name: required
email: required
age: negative
---
true
true

errors.Join zwraca nil, gdy każdy argument jest nil, więc powyższa funkcja nie potrzebuje osobnego przypadku dla "brak błędów".

Metody Unwrap i Is

errors.Is i errors.As znajdują opakowane błędy, wywołując metodę Unwrap. Własny typ, który przechowuje przyczynę, powinien ją udostępniać:

Typ opakowujący kilka błędów implementuje zamiast tego Unwrap() []error.

Typ może też zdefiniować Is(target error) bool, żeby sam decydować o równości, na przykład żeby pasował do każdego *HTTPError z tym samym kodem statusu. Rzadko jest to potrzebne; zwykle wystarczy opakować sentinel i zwrócić go z Unwrap.

Pułapka typowanego nil

Nigdy nie deklaruj zmiennej błędu z konkretnym typem, żeby potem zwracać ją jako error:

func check() error {
	var err *ValidationError // nil pointer
	// ... no problem found
	return err // non-nil error! Its type is *ValidationError
}

U wywołującego if err != nil jest prawdą, bo interfejs zawierający typowany wskaźnik nil nie jest nil. Przy sukcesie zwracaj dosłowne nil, a lokalne zmienne błędów typuj jako error. Strona o interfejsach wyjaśnia dlaczego.

Który rodzaj wybrać

Wywołujący musząZapewnij
tylko zalogować lub wyświetlić błądfmt.Errorf("...: %w", err)
rozgałęzić kod dla jednego konkretnego warunkusentinel var ErrX = errors.New(...)
odczytać szczegóły błędutyp błędu z polami
zobaczyć kilka niezależnych błędówerrors.Join

Typowe błędy

  • Porównywanie opakowanych błędów przez ==. Użyj errors.Is.
  • Przekazanie do errors.As czegoś, co nie jest wskaźnikiem. Funkcja potrzebuje wskaźnika na zmienną typu docelowego.
  • Tworzenie "sentinela" wewnątrz funkcji. return errors.New("not found") przy każdym wywołaniu tworzy nową wartość; wywołujący nie mogą się z nią porównać.
  • Eksportowanie każdego błędu. Każdy eksportowany sentinel czy typ to obietnica w API.
  • Dopasowywanie po err.Error(). Stringi są dla ludzi.

Najczęściej zadawane pytania

Jak utworzyć własny błąd w Go?

Dla stałego warunku zadeklaruj sentinel na poziomie pakietu: var ErrNotFound = errors.New("not found"). Dla błędu, który niesie dane, zdefiniuj typ z metodą Error() string: type ValidationError struct { Field string } oraz func (e *ValidationError) Error() string { return e.Field + " is invalid" }.

Jak opakować błąd w Go?

Użyj fmt.Errorf z czasownikiem %w: return fmt.Errorf("load user %d: %w", id, err). Komunikat nowego błędu zawiera stary, a errors.Unwrap, errors.Is i errors.As mogą dotrzeć do oryginału. Od Go 1.20 jedno wywołanie Errorf może opakować kilka błędów kilkoma czasownikami %w.

Czym różni się errors.Is od errors.As?

errors.Is(err, target) odpowiada na pytanie "czy ta konkretna wartość błędu jest gdzieś w łańcuchu", dla sentinelów takich jak io.EOF. errors.As(err, &target) odpowiada na pytanie "czy w łańcuchu jest błąd tego typu", a jeśli tak, zapisuje go w target, żeby można było odczytać jego pola.

Co robi errors.Join?

errors.Join(errs...) (Go 1.20) łączy kilka błędów w jeden. Jego komunikat to poszczególne komunikaty rozdzielone znakami nowej linii, błędy nil są pomijane, a jeśli wszystkie są nil, zwraca nil. errors.Is i errors.As pasują, jeśli pasuje którykolwiek z połączonych błędów.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ