Jak wygląda panika
Panika zatrzymuje bieżącą funkcję, wykonuje jej wywołania odroczone, a potem robi to samo w funkcji wywołującej i tak dalej w górę stosu. Jeśli dotrze na szczyt gorutyny, program się wywraca.
Ten program kończy się ze statusem 2. Wynik:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
Wywołanie odroczone wykonało się przed raportem o awarii. Ślad stosu podaje gorutynę, funkcję i linię, co zwykle wystarcza, żeby znaleźć błąd.
Typowe paniki w czasie działania
| Komunikat | Przyczyna |
|---|---|
index out of range [5] with length 3 | indeks slice'a, tablicy lub stringa za końcem |
slice bounds out of range [:7] with capacity 5 | wycinanie poza pojemność |
invalid memory address or nil pointer dereference | odczyt pola lub wywołanie przez wskaźnik nil |
assignment to entry in nil map | zapis do mapy, która nigdy nie została utworzona |
interface conversion: interface {} is int, not string | jednowartościowa asercja typu na zły typ |
integer divide by zero | dzielenie całkowite lub modulo przez 0 (liczby zmiennoprzecinkowe dają zamiast tego +Inf albo NaN) |
close of closed channel, send on closed channel | niewłaściwe użycie kanału |
all goroutines are asleep - deadlock! | wszystkie gorutyny zablokowane (błąd krytyczny, a nie panika) |
Każda z nich to błąd w programie, a nie sytuacja do obsłużenia. Poprawką jest sprawdzenie zakresu, sprawdzenie nil, make albo asercja comma-ok, a nie recover.
Przechwytywanie
recover() zatrzymuje panikę. Działa tylko wywołane bezpośrednio wewnątrz funkcji odroczonej, bo funkcje odroczone to jedyny kod, który wykonuje się podczas zwijania stosu przez panikę.
Wynik:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Co się stało w drugim wywołaniu:
a / bwywołało panikę.- Wykonało się odroczone domknięcie, a
recover()zwróciło wartość paniki (runtime.Error). - Zwijanie stosu się zatrzymało.
safeDividenormalnie wróciło domainz nazwanym wynikiemerrustawionym przez domknięcie.
To nazwany wynik pozwala funkcji odroczonej oddać błąd. Bez niego funkcja zwraca swoje wartości zerowe. Strona o defer opisuje, jak odroczone domknięcia zmieniają wyniki.
recover() zwraca nil, gdy nie ma paniki, więc sprawdzenie if r != nil sprawia, że funkcja odroczona jest nieszkodliwa na normalnej ścieżce. Wywołane poza funkcją odroczoną albo w funkcji wywołanej przez funkcję odroczoną recover zwraca nil i nic nie robi.
panic z własną wartością
panic przyjmuje dowolną wartość. Typowo jest to błąd albo string.
Przechwytuj to, czego się spodziewasz, a wszystko inne puszczaj dalej ponownym panic. Połykanie każdej paniki ukrywa prawdziwe błędy.
Od Go 1.21 panic(nil) zamienia się w *runtime.PanicNilError, więc nil zwrócone przez recover() teraz niezawodnie oznacza „brak paniki”.
Paniki w gorutynach
recover łapie paniki tylko we własnej gorutynie. Panika w dowolnej gorutynie bez recover zabija cały proces, łącznie z main i wszystkimi innymi gorutynami.
Dwie linie workerów mogą pojawić się w dowolnej kolejności; main finished zawsze jest ostatnie. defer recover() w main nie uratowałoby programu przed drugim workerem. Dlatego serwery HTTP przechwytują paniki osobno dla każdego żądania: net/http przechwytuje paniki w gorutynie każdego handlera, loguje je i zamyka to połączenie, więc jedno złe żądanie nie kładzie serwera.
Niektóre awarie to błędy krytyczne, a nie paniki, i w ogóle nie da się ich przechwycić: concurrent map writes, brak pamięci i all goroutines are asleep z detektora zakleszczeń.
Kiedy panika jest właściwym wyborem
Ogólna zasada Go: dla wszystkiego, co może pójść źle w czasie działania, zwracaj błędy, a panikuj tylko przy pomyłkach programisty. Konkretnie panika jest na miejscu, gdy:
- Złamany jest niezmiennik.
switchpo twoim własnym enumie trafia na przypadek, który nie może się zdarzyć. Kontynuowanie uszkodziłoby dane. - Funkcja pomocnicza
Mustdostaje złą stałą.regexp.MustCompile,template.Mustiuuid.MustParseopakowują funkcję zwracającą błąd i wywołują panikę przy niepowodzeniu. Używaj ich dla wartości znanych w czasie kompilacji, zwykle zmiennych na poziomie pakietu, gdzie niepowodzenie oznacza, że błędny jest kod źródłowy:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Start nie może być kontynuowany. Brak wymaganej konfiguracji w
main. Nawet tu wypisanie błędu i wywołanieos.Exit(1)jest często czystsze niż ślad stosu.
Panika to złe narzędzie do:
- Spodziewanych niepowodzeń: niepoprawne dane od użytkownika, brakujący plik, timeout. Zwróć
error; zobacz obsługę błędów. - Sterowania przepływem: używanie panic i recover jak wyjątków w dużym drzewie wywołań sprawia, że kod trudno śledzić. Biblioteka standardowa robi tak wewnętrznie w kilku miejscach (enkoder
encoding/json), zawsze przechwytując panikę przed powrotem, więc żadna panika nie wychodzi poza pakiet. - API bibliotek: biblioteka, która panikuje przy złych danych, zmusza każdego wywołującego do dodawania recover. Zwróć błąd.
Częste błędy
- Wywołanie recover poza funkcją odroczoną. Zwraca
nil. - Przechwytywanie w
mainpaniki z gorutyny. Każda gorutyna potrzebuje własnego recover. - Połykanie wszystkich panik. Loguj je ze stosem (
debug.Stack()zruntime/debug) i ponownie wywołuj panikę dla tego, czego się nie spodziewasz. - Używanie
recoverdo obsługi zapisów do mapy nil albo indeksów poza zakresem. Zamiast tego popraw błąd.
Najczęściej zadawane pytania
Czym jest panic w Go?
To awaria w czasie działania, która zatrzymuje normalny przepływ bieżącej gorutyny. Go wykonuje wywołania odroczone każdej funkcji na stosie, od najgłębszej na zewnątrz, a jeśli nic nie przechwyci paniki, program wypisuje wartość paniki i ślad stosu i kończy się ze statusem 2. Paniki biorą się z błędów (indeks poza zakresem, dereferencja wskaźnika nil, zapis do mapy nil) albo z jawnego wywołania panic(v).
Jak przechwycić panikę w Go?
Wywołaj recover() wewnątrz funkcji odroczonej: defer func() { if r := recover(); r != nil { ... } }(). Zwraca wartość przekazaną do panic i zatrzymuje zwijanie stosu, więc funkcja, która ją odroczyła, normalnie wraca do kodu wywołującego. Wywołane gdziekolwiek indziej recover zwraca nil i nic nie robi.
Czy mogę przechwycić panikę z innej gorutyny?
Nie. recover zatrzymuje panikę tylko w gorutynie, w której działa. Panika w uruchomionej przez ciebie gorutynie, bez recover wewnątrz tej gorutyny, wywraca cały program. Każda gorutyna, która może wpaść w panikę, potrzebuje własnego odroczonego recover.
Kiedy używać panic zamiast zwracania błędu?
Przy błędach programisty i niemożliwych stanach, a nie przy spodziewanych niepowodzeniach. Złe dane wejściowe, brakujące pliki i błędy sieci to błędy (error). Panika jest rozsądna, gdy złamany jest niezmiennik, gdy funkcja pomocnicza Must dostaje stałą, która zawsze powinna być poprawna (regexp.MustCompile), albo gdy program w ogóle nie może wystartować. Biblioteki nie powinny wypuszczać paniki poza swoje publiczne API.