Dlaczego w Go nie ma Try-Catch?
go celowo nie wspiera try-catch, a jedynie składnię panic-recover. Budzi to niezadowolenie wielu programistów przyzwyczajonych do obsługi błędów za pomocą try-catch. Dlaczego zatem nie zaimplementowano try-catch? Ponieważ try-catch posiada wiele problemów.
try-catch-finally
Konstrukcja try-catch służy do obsługi sytuacji błędów i wyjątków, które mogą wystąpić podczas wykonywania programu (runtime). Dodatkowo, w bloku finally umieszcza się kod, który musi zostać wykonany niezależnie od tego, czy wystąpił wyjątek.
Obsługa błędów i odpowiedzialność
W latach 80. i 90. obsługa błędów była bardzo prosta. Komunikaty o błędach ograniczały się do 'Floppy disk full', 'No floppy disk in drive', 'No write permission on floppy disk', a programiści w tamtych czasach, w przypadku wystąpienia błędu, rzucali go do punktu obsługi błędów w celu scentralizowanego przetwarzania. W takich okolicznościach konstrukcja try-catch działała efektywnie.
Jednak z biegiem czasu sytuacja uległa zmianie. Logika biznesowa stała się bardziej złożona, pojawiły się bazy danych i transakcje, trzeba było interpretować liczne komunikaty żądań podczas wywoływania wielu API poprzez sieć, a nawet, wraz z pojawieniem się programowania współbieżnego, błędy musiały być obsługiwane w innych wątkach niż główny.
Błędy stały się zbyt skomplikowane, aby można je było obsługiwać w jednym miejscu, i nikt nie mógł wziąć odpowiedzialności za wszystkie błędy. W tym momencie pojawia się poważny problem z try-catch.
Przeniesienie odpowiedzialności w try-catch
try-catch, krótko mówiąc, jest metodą, w której podmiot, który spowodował błąd, przenosi odpowiedzialność (następstwa) za jego wystąpienie na kogoś innego. Tym odbiorcą może być blok catch, jego metoda nadrzędna, lub czyjś rodzic, rodzic rodzica, rodzic rodzica rodzica... Innymi słowy, w świecie, gdzie obsługa błędów staje się coraz liczniejsza i bardziej złożona, metoda wybrana przez try-catch to 'ktoś to zrobi'. 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 z powyższym kodem polega na tym, że nie jest jasne, kto odpowiada za printStackTrace, ani w jakim kodzie wystąpił błąd. Co więcej, im więcej logiki pojawia się w instrukcji try, tym problem staje się straszniejszy. Jednak, paradoksalnie, im bardziej złożone stawało się programowanie, tym bardziej programiści uzależniali się od konstrukcji try-catch, przenoszącej odpowiedzialność, nie zastanawiali się nad obsługą błędów, ich poczucie odpowiedzialności słabło, a w końcu zapomnieli 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 wiele systemów, które mają na celu zapobieganie błądzeniu programistów na złe ścieżki i kształtowanie ich w dobrych programistów. panic - recover również może być tego przykładem. Panic i recover na pierwszy rzut oka wydają się nie różnić od try-catch, jednak różnią się tym, że w przypadku wystąpienia problemu nie przenoszą odpowiedzialności na zewnątrz. Gdy w go występuje panic, zamiast przekazywać tę wartość na zewnątrz, problem powinien zostać rozwiązany w miejscu, w którym wystąpił panic. Nakłada to na programistę odpowiedzialność za obsługę błędów, skłaniając go do głębszego zastanowienia się, gdzie, kto i jak powinien obsłużyć dany błąd. Zapewnia to użytkownikowi autonomię, pozostawiając jednocześnie pole do namysłu.
Ponadto, wybór słowa "panic" jest niezwykle trafny, ponieważ w przeciwieństwie do try-recover, panic-recover już samym słowem wywiera na programistę presję, aby używać go tylko w sytuacjach ewidentnych błędów, a nie tylko w sytuacjach wyjątkowych. Naturalnie, programiści nie nadużywają recover, używając go tylko tam, gdzie jest to konieczne. To znacząco przyczynia się do tworzenia zwięzłego kodu w go.