GoSuda

Защо Go предпочита явно връщане на грешки и panic-recover пред хвърляне на изключения чрез try-catch

By Rabbit Princess views ...

Go умишлено не поддържа try-catch, а предоставя единствено синтаксиса panic-recover. Това е предизвикало недоволство у множество разработчици, които са свикнали с обработката на грешки чрез try-catch. Тогава възниква въпросът защо try-catch не е включен? Причината е, че try-catch има редица проблеми.

try-catch-finally

Конструкцията try-catch (трайкетч) служи за обработка на грешни и изключителни ситуации, които могат да възникнат по време на изпълнение на програмата (в рънтайма). Блокът finally пък съдържа код, който трябва да се изпълни безусловно, независимо от това дали е възникнало изключение.

Обработка на грешки и отговорност

През 80-те и 90-те години обработката на грешки беше изключително проста. Съобщенията за грешка се ограничаваха до „Дискетата е пълна", „Няма дискета в устройството" и „Няма разрешение за запис на дискетата", и разработчиците от онова време, когато възникнеше грешка, я изхвърляха (throw) до мястото на обработка, където тя се обединяваше и обработваше централизирано. При тези условия try-catch конструкцията работеше ефективно.

Въпреки това с течение на времето ситуацията се промени. Бизнес логиката стана сложна, появиха се бази данни и транзакции, чрез мрежата се извикваха множество API, като трябваше да се интерпретират огромен брой заявки, а с появата на конкурентното програмиране грешките понякога трябваше да се обработват в нишки, различни от основната.

Грешките станаха твърде сложни, за да бъдат обработвани на едно място, и нито едно място вече не можеше да поеме отговорността за всички грешки. Именно тук try-catch започва да създава сериозен проблем.

Прехвърлянето на отговорност при try-catch

Казано накратко, try-catch е начин обектът, предизвикал грешката, да прехвърли отговорността за нея (последващата обработка) към някой друг. Този „някой друг" може да бъде блокът catch, родителският метод, родителят на родителя на родителя на родителя... или някой друг още по-горе. С други думи, в свят, в който обработката на грешки става все по-голяма и по-сложна, методът, който try-catch избира, е именно „някой ще се погрижи". Нека разгледаме следния код.

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}

Проблемът с този код е, че не е ясно кой отговаря за обработката на printStackTrace и също така не може да се установи в кой от кодовите фрагменти е възникнала грешката. Още по-лошо, колкото повече логика се добавя в блока try, толкова по-ужасяващ става проблемът. Парадоксално обаче, колкото по-сложна става разработката, толкова повече разработчиците се пристрастяват към try-catch конструкцията, която прехвърля отговорността, престават да се замислят за обработката на грешки, отговорността им намалява и в крайна сметка забравят същността на обработката на грешки и изключенията. Как тогава golang решава този проблем?

panic, recover

Една от предимствата на Go е, че разполага с множество системи, които не позволяват на разработчика да поеме по лош път, а го насочват към това да стане добър разработчик. Механизмът panic-recover е един от примерите за това.

Механизмите panic и recover на пръв поглед не се различават от try-catch, но се отличават по това, че при възникване на проблем не прехвърлят отговорността навън. В Go, когато възникне panic, стойността не се подава навън, а проблемът трябва да бъде решен на мястото, където е възникнал panic. Това налага на разработчика да понесе отговорността за обработката на грешката и го насърчава да обмисли по-задълбочено къде, кой и как трябва да я обработи. Така се гарантира автономия на потребителя, като същевременно се оставя място за размисъл.

Освен това изборът на думата panic е особено удачен, защото за разлика от try-recover, панирането с panic-recover създава у разработчика усещането, че трябва да се използва единствено при ясни грешни ситуации, а не при изключения. По този начин разработчикът естествено не злоупотребява с recover и го използва само там, където е необходимо. Това силно допринася за писането на кратък и ясен код в Go.