Obsługa błędów w Golangu

W przeciwieństwie do konwencjonalnych metod w innych językach programowania głównego nurtu, takich jak JavaScript (który używa instrukcji try… catch) czy Python (z blokiem try… except), radzenie sobie z błędami w Go wymaga innego podejścia. Dlaczego? Ponieważ jego funkcje do obsługi błędów są często źle stosowane.

W tym wpisie na blogu przyjrzymy się najlepszym praktykom, które można wykorzystać do obsługi błędów w aplikacji Go. Podstawowe zrozumienie tego, jak działa Go, to wszystko, co jest wymagane, aby przetrawić ten artykuł – jeśli utkniesz w jakimś punkcie, dobrze jest poświęcić trochę czasu i zbadać nieznane koncepcje.

Bezpośredni identyfikator

Bezpośredni identyfikator jest anonimowym wskaźnikiem miejsca. Może być używany jak każdy inny identyfikator w deklaracji, ale nie wprowadza wiązania. Pusty identyfikator zapewnia sposób na zignorowanie lewych wartości w przypisaniu i uniknięcie błędów kompilatora o nieużywanych importach i zmiennych w programie. Praktyka przypisywania błędów do pustego identyfikatora zamiast ich właściwej obsługi jest niebezpieczna, ponieważ oznacza to, że zdecydowałeś się jawnie zignorować wartość zdefiniowanej funkcji.

result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}

Twoim powodem, dla którego prawdopodobnie to robisz, jest to, że nie spodziewasz się błędu z funkcji (lub jakiegokolwiek błędu, który może wystąpić), ale może to spowodować efekty kaskadowe w twoim programie. Najlepszą rzeczą do zrobienia jest obsługa błędu, kiedy tylko możesz.

Obsługa błędów poprzez wielokrotne wartości zwracane

Jednym ze sposobów obsługi błędów jest skorzystanie z faktu, że funkcje w Go obsługują wiele wartości zwracanych. Dzięki temu możesz przekazać zmienną błędu wraz z wynikiem funkcji, którą definiujesz:

func iterate(x, y int) (int, error) {}

W powyższym przykładzie kodu musimy zwrócić predefiniowaną zmienną error, jeśli uważamy, że istnieje szansa, że nasza funkcja może zawieść. error jest typem interfejsu zadeklarowanym w pakiecie Go built-in, a jego wartość zerowa to nil.

type error interface { Error() string }

Zwykle zwrócenie błędu oznacza, że wystąpił problem, a zwrócenie nil oznacza, że nie wystąpiły żadne błędy:

result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }

Więc za każdym razem, gdy funkcja iterate zostanie wywołana i err nie będzie równe nil, zwrócony błąd powinien zostać odpowiednio obsłużony – opcją może być utworzenie instancji mechanizmu retry lub cleanup. Jedyną wadą obsługi błędów w ten sposób jest to, że nie ma egzekwowania od kompilatora Go, musisz zdecydować, w jaki sposób utworzona funkcja zwraca błąd. Możesz zdefiniować strukturę błędu i umieścić ją w pozycji zwracanych wartości. Jednym ze sposobów, aby to zrobić, jest użycie wbudowanego errorString struct (możesz również znaleźć ten kod w kodzie źródłowym Go):

package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }

W powyższej próbce kodu, errorString osadza string, który jest zwracany przez metodę Error. Aby utworzyć niestandardowy błąd, musisz zdefiniować strukturę błędu i użyć zestawów metod, aby powiązać funkcję z twoją strukturą:

// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}

Nowo utworzony niestandardowy błąd można następnie zrestrukturyzować, aby użyć wbudowanej struktury error:

 import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}

Jednym z ograniczeń wbudowanej struktury error jest to, że nie zawiera ona śladów stosu. To sprawia, że zlokalizowanie miejsca wystąpienia błędu jest bardzo trudne. Błąd może przejść przez wiele funkcji zanim zostanie wypisany. Aby sobie z tym poradzić, możesz zainstalować pakiet pkg/errors, który zapewnia podstawowe prymitywy obsługi błędów, takie jak rejestrowanie śladów stosu, zawijanie i rozwijanie błędów oraz formatowanie. Aby zainstalować ten pakiet, wykonaj to polecenie w terminalu:

go get github.com/pkg/errors

Gdy potrzebujesz dodać do swoich błędów ślady stosu lub inne informacje ułatwiające debugowanie, użyj funkcji New lub Errorf, aby zapewnić błędy, które rejestrują twój ślad stosu. Errorf implementuje interfejs fmt.Formatter, który pozwala formatować błędy przy użyciu run pakietu fmt (%s, %v, %+v itd.):

import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}

Aby wydrukować ślady stosu zamiast zwykłego komunikatu o błędzie, musisz użyć %+v zamiast %v we wzorcu formatu, a ślady stosu będą wyglądać podobnie do poniższej próbki kodu:

Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Defer, panic, and recover

Chociaż Go nie posiada wyjątków, to posiada podobny rodzaj mechanizmu znanego jako „Defer, panic, and recover”. Ideologią Go jest to, że dodanie wyjątków takich jak try/catch/finally w JavaScript spowodowałoby skomplikowanie kodu i zachęciło programistów do oznaczania zbyt wielu podstawowych błędów, takich jak nieudane otwarcie pliku, jako wyjątkowe. Nie powinieneś używać defer/panic/recover tak jak throw/catch/finally; tylko w przypadkach nieoczekiwanej, niemożliwej do naprawienia awarii.

Defer jest mechanizmem językowym, który umieszcza wywołanie funkcji na stosie. Każda odroczona funkcja jest wykonywana w odwrotnej kolejności, gdy funkcja hosta kończy się, niezależnie od tego, czy wywołana jest panika, czy nie. Mechanizm odraczania jest bardzo przydatny do czyszczenia zasobów:

package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}

To skompilowałoby się jako:

If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!

Panic jest wbudowaną funkcją, która zatrzymuje normalny przepływ wykonania. Kiedy nazywasz panic w swoim kodzie, oznacza to, że zdecydowałeś, że twój rozmówca nie może rozwiązać problemu. Tak więc panic powinno być używane tylko w rzadkich przypadkach, gdy nie jest bezpieczne dla twojego kodu lub kogokolwiek integrującego twój kod, aby kontynuować w tym momencie. Oto próbka kodu przedstawiająca, jak działa panic:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Powyższa próbka skompilowałaby się jako:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.

Jak pokazano powyżej, gdy panic jest używane i nie jest obsługiwane, przepływ wykonania zatrzymuje się, wszystkie funkcje odroczone są wykonywane w odwrotnej kolejności, a ślady stosu są drukowane.

Możesz użyć wbudowanej funkcji recover do obsługi panic i zwrócenia wartości przechodzących z wywołania paniki. recover musi być zawsze wywoływana w funkcji defer inaczej zwróci nil:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Jak widać w próbce kodu powyżej, recover zapobiega zatrzymaniu całego przepływu wykonania, ponieważ wrzuciliśmy funkcję panic i kompilator zwróciłby:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.

Aby zgłosić błąd jako wartość zwracaną, musisz wywołać funkcję recover w tej samej goroutine, w której wywoływana jest funkcja panic, pobrać strukturę błędu z funkcji recover i przekazać ją do zmiennej:

package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}

Każda funkcja odroczona zostanie wykonana po wywołaniu funkcji, ale przed deklaracją zwrotu. Tak więc, możesz ustawić zmienną zwracaną zanim instrukcja return zostanie wykonana. Powyższa próbka kodu skompilowałaby się jako:

If it's more than 100 degrees...Then there's nothing we can doProgram exited.

Owijanie błędów

Poprzednio owijanie błędów w Go było dostępne tylko poprzez użycie pakietów takich jak pkg/errors. Jednakże, w najnowszym wydaniu Go – wersji 1.13, wsparcie dla zawijania błędów jest obecne. Zgodnie z uwagami do wydania:

Błąd e może zawijać inny błąd w poprzez dostarczenie metody Unwrap, która zwraca w. Zarówno e, jak i w są dostępne dla programów, pozwalając e na dostarczenie dodatkowego kontekstu do w lub na jego reinterpretację, jednocześnie nadal pozwalając programom na podejmowanie decyzji na podstawie w.

Aby tworzyć błędy opakowane, fmt.Errorf ma teraz czasownik %w, a do inspekcji i rozpakowywania błędów dodano kilka funkcji do pakietu error:

errors.Unwrap: Ta funkcja w zasadzie sprawdza i ujawnia podstawowe błędy w programie. Zwraca ona wynik wywołania metody Unwrap na Err. Jeśli typ Err zawiera metodę Unwrap zwracającą błąd. W przeciwnym razie Unwrap zwraca nil.

package errorstype Wrapper interface{ Unwrap() error}

Poniżej znajduje się przykładowa implementacja metody Unwrap:

func(e*PathError)Unwrap()error{ return e.Err}

errors.Is: Za pomocą tej funkcji można porównać wartość błędu z wartością sentinel. To, co odróżnia tę funkcję od naszych zwykłych kontroli błędów, to fakt, że zamiast porównywać wartość sentinel do jednego błędu, porównuje ją do każdego błędu w łańcuchu błędów. Implementuje ona również metodę Is na błędzie, tak że błąd może się zgłosić jako wartość sentinel, nawet jeśli nie jest wartością sentinel.

func Is(err, target error) bool

W podstawowej implementacji powyżej, Is sprawdza i zgłasza, czy err lub którykolwiek z errors w jego łańcuchu są równe target (wartość sentinel).

errors.As: Ta funkcja zapewnia sposób rzutowania na określony typ błędu. Szuka pierwszego błędu w łańcuchu błędów, który pasuje do wartości sentinel i jeśli zostanie znaleziony, ustawia wartość sentinel na tę wartość błędu i zwraca true:

package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}

Możesz znaleźć ten kod w kodzie źródłowym Go.

Wynik kompilatora:

Failed at path: non-existingProgram exited.

Błąd pasuje do wartości sentinel, jeśli konkretna wartość błędu jest przypisywalna do wartości wskazywanej przez wartość sentinel. As wpadnie w panikę, jeśli wartość sentinel nie jest wskaźnikiem non-nil do typu implementującego błąd lub do dowolnego typu interfejsu. As zwraca false, jeśli err jest nil.

Podsumowanie

Społeczność Go poczyniła ostatnio imponujące postępy w obsłudze różnych koncepcji programowania i wprowadzaniu jeszcze bardziej zwięzłych i łatwych sposobów obsługi błędów. Czy masz jakieś pomysły na to, jak radzić sobie z błędami, które mogą pojawić się w Twoim programie Go? Daj mi znać w komentarzach poniżej.

Źródła:
Specyfikacja języka programowania Go dotycząca asercji typu
Przemówienie Marcela van Lohuizena na dotGo 2019 – wartości błędów Go 2 już dziś
Go 1.13 release notes

LogRocket: Full visibility into your web apps

LogRocket to rozwiązanie do monitorowania aplikacji frontendowych, które pozwala Ci odtwarzać problemy tak, jakby działy się we własnej przeglądarce. Zamiast zgadywać, dlaczego zdarzają się błędy lub prosić użytkowników o zrzuty ekranu i logi, LogRocket pozwala Ci odtworzyć sesję, aby szybko zrozumieć, co poszło nie tak. Działa doskonale z każdą aplikacją, niezależnie od frameworka, i posiada wtyczki do rejestrowania dodatkowego kontekstu z Reduxa, Vuexa i @ngrx/store.

Oprócz rejestrowania akcji i stanu Reduxa, LogRocket rejestruje logi konsoli, błędy JavaScript, stacktraces, żądania/odpowiedzi sieciowe z nagłówkami + ciałami, metadane przeglądarki i niestandardowe logi. Instrumentuje również DOM, aby nagrać HTML i CSS na stronie, odtwarzając pikselowo doskonałe filmy nawet najbardziej złożonych aplikacji jednostronicowych.

Wypróbuj za darmo.

Wykorzystaj to za darmo.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.