GoSuda

Dlaczego Go wybrało jawne zwracanie błędów oraz panic-recover zamiast rzucania wyjątków try-catch

By Rabbit Princess views ...

Go celowo nie obsługuje mechanizmu try-catch, a wspiera jedynie składnię panic-recover. Wzbudziło to niezadowolenie wielu programistów, którzy są przyzwyczajeni do obsługi błędów za pomocą try-catch. Dlaczego zatem try-catch nie został włączony? Powodem jest to, że try-catch ma wiele problemów.

try-catch-finally

Konstrukcja try-catch (try-catch) służy do obsługi sytuacji błędnych i wyjątkowych, które mogą wystąpić podczas działania programu (w czasie wykonywania, runtime). Z kolei blok finally zawiera kod, który musi zostać wykonany bezwarunkowo, niezależnie od tego, czy wystąpił wyjątek.

Obsługa błędów a odpowiedzialność

W latach 80. i 90. obsługa błędów była bardzo prosta. Komunikaty o błędach ograniczały się do: „Dyskietka pełna", „Brak dyskietki w napędzie" oraz „Brak uprawnień do zapisu na dyskietce". Programista w tamtych czasach, gdy wystąpił błąd, przekazywał go (throw) do miejsca obsługi błędów, gdzie był przetwarzany w sposób ujednolicony. W takiej sytuacji try-catch działał efektywnie.

Jednak z biegiem czasu sytuacja się zmieniła. Logika biznesowa stała się złożona, pojawiły się bazy danych i transakcje, a przez sieć wywoływano wiele interfejsów API, co wymagało interpretacji wielu komunikatów żądań. Ponadto wraz z pojawieniem się programowania współbieżnego błędy musiały być obsługiwane w wątkach innych niż główny.

Błędy stały się zbyt złożone, aby można je było obsłużyć w jednym miejscu, i żadne pojedyncze miejsce nie mogło ponosić odpowiedzialności za wszystkie błędy. W tym miejscu pojawia się poważny problem z try-catch.

Przerzucanie odpowiedzialności przez try-catch

Mówiąc krótko, try-catch jest sposobem, w którym element zgłaszający błąd przerzuca odpowiedzialność za jego skutki (dalsze postępowanie) na kogoś innego. Tym kimś może być blok catch, metoda nadrzędna, metoda nadrzędna metody nadrzędnej, a nawet ktoś jeszcze wyżej w hierarchii wywołań... Innymi słowy, w świecie, w którym obsługa błędów staje się coraz liczniejsza i bardziej skomplikowana, try-catch przyjął podejście „ktoś na pewno się tym zajmie". Spójrzmy na poniższy kod.

1try {
2    data = readFile("hello.txt");
3    structuredData = parseData(data);
4    insertDBStatus(structuredData[1]);
5    startHTTPServer(structuredData[2]);
6} catch (Exception e) {
7    e.printStackTrace();
8}

Problem tego kodu polega na tym, że nie wiadomo jednoznacznie, kto odpowiada za obsługę printStackTrace, ani nie da się ustalić, w którym fragmencie kodu wystąpił błąd. Co więcej, im więcej logiki znajduje się w bloku try, tym problem staje się jeszcze bardziej dotkliwy. Paradoksalnie jednak, im bardziej rozwijał się rozwój oprogramowania, tym bardziej programiści uzależniali się od try-catch, który przerzucał odpowiedzialność, przestawali zastanawiać się nad obsługą błędów, słabła ich odpowiedzialność i ostatecznie zapominali o istocie obsługi błędów i wyjątków. Jak zatem golang rozwiązał ten problem?

panic, recover

Jedną z zalet Go jest to, że posiada różne mechanizmy, które nie pozwalają programiście zboczyć na złą drogę, lecz pomagają mu stać się dobrym programistą. Przykładem są właśnie panic i recover.

Na pierwszy rzut oka panic-recover niewiele różni się od try-catch, jednak różni się tym, że w przypadku wystąpienia problemu nie przerzuca odpowiedzialności na zewnątrz. W Go, gdy wystąpi panic, zamiast przekazywać wartość na zewnątrz, należy rozwiązać problem w miejscu, w którym panika wystąpiła. Daje to programiście odpowiedzialność za obsługę błędów i zachęca go do bardziej dogłębnego zastanowienia się nad tym, gdzie, kto i w jaki sposób powinien obsłużyć dany błąd. Jest to swego rodzaju zapewnienie autonomii użytkownikowi, a jednocześnie pozostawienie mu miejsca na przemyślenie decyzji.

Nazwa panic również jest wyjątkowo trafnie dobrana. W przeciwieństwie do try-recover, panic-recover narzuca programiście presję, by używać go wyłącznie w sytuacjach jednoznacznie błędnych, a nie w sytuacjach jedynie wyjątkowych. Naturalnie programista nie nadużywa recover i stosuje go tylko tam, gdzie jest to konieczne. Ma to duże znaczenie dla pisania zwięzłego kodu w Go.