Trzy rodzaje błędów
Kod w Go używa trzech postaci błędów, od najprostszej do najbogatszej:
- Błąd doraźny:
errors.New("...")lubfmt.Errorf("...")utworzony na miejscu. Wywołujący mogą tylko przeczytać komunikat. - Błąd sentinel: zmienna na poziomie pakietu, taka jak
io.EOF. Wywołujący mogą ją sprawdzić przezerrors.Is. - Typ błędu: struktura z metodą
Error(), zawierająca pola. Wywołujący mogą ją wyciągnąć przezerrors.Asi 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łąd | fmt.Errorf("...: %w", err) |
| rozgałęzić kod dla jednego konkretnego warunku | sentinel var ErrX = errors.New(...) |
| odczytać szczegóły błędu | typ błędu z polami |
| zobaczyć kilka niezależnych błędów | errors.Join |
Typowe błędy
- Porównywanie opakowanych błędów przez
==. Użyjerrors.Is. - Przekazanie do
errors.Asczegoś, 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.