Menu

Panic i recover w Go: paniki runtime i kiedy panikować

Panika zatrzymuje normalne wykonanie i zwija stos, uruchamiając wywołania odroczone. Dowiedz się, co wywołuje paniki, jak recover w funkcji odroczonej je zatrzymuje, jakie komunikaty błędów runtime zobaczysz i kiedy panika jest właściwym wyborem.

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

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

KomunikatPrzyczyna
index out of range [5] with length 3indeks slice'a, tablicy lub stringa za końcem
slice bounds out of range [:7] with capacity 5wycinanie poza pojemność
invalid memory address or nil pointer dereferenceodczyt pola lub wywołanie przez wskaźnik nil
assignment to entry in nil mapzapis do mapy, która nigdy nie została utworzona
interface conversion: interface {} is int, not stringjednowartościowa asercja typu na zły typ
integer divide by zerodzielenie całkowite lub modulo przez 0 (liczby zmiennoprzecinkowe dają zamiast tego +Inf albo NaN)
close of closed channel, send on closed channelniewł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:

  1. a / b wywołało panikę.
  2. Wykonało się odroczone domknięcie, a recover() zwróciło wartość paniki (runtime.Error).
  3. Zwijanie stosu się zatrzymało. safeDivide normalnie wróciło do main z nazwanym wynikiem err ustawionym 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. switch po twoim własnym enumie trafia na przypadek, który nie może się zdarzyć. Kontynuowanie uszkodziłoby dane.
  • Funkcja pomocnicza Must dostaje złą stałą. regexp.MustCompile, template.Must i uuid.MustParse opakowują 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łanie os.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 main paniki z gorutyny. Każda gorutyna potrzebuje własnego recover.
  • Połykanie wszystkich panik. Loguj je ze stosem (debug.Stack() z runtime/debug) i ponownie wywołuj panikę dla tego, czego się nie spodziewasz.
  • Używanie recover do 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ