GoSuda

Причины, по которым Go выбрал явный возврат ошибок и механизм panic-recover вместо генерации исключений try-catch

By Rabbit Princess views ...

Go намеренно не поддерживает try-catch и предоставляет только синтаксис panic-recover. Это вызывает недовольство множества разработчиков, привыкших к обработке ошибок с помощью try-catch. Так почему же в Go не включили 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, тем ужаснее становится ситуация. Но парадоксальным образом, чем сложнее становилась разработка, тем сильнее разработчики зависали на трайкэтч-конструкции, перекладывающей ответственность, перестали задумываться об обработке ошибок, ослабло чувство ответственности и в итоге была утрачена суть обработки ошибок и исключений. Как же Golang решил эту проблему?

panic, recover

Одно из преимуществ Go заключается в том, что в языке есть различные механизмы, которые не позволяют разработчику скатиться на дурной путь и помогают стать хорошим разработчиком. Примером тому служат panic и recover.

На первый взгляд panic и recover ничем не отличаются от try-catch, однако они отличаются тем, что при возникновении проблемы ответственность не перекладывается на внешний код. В Go при возникновении panic значение не передаётся наружу, а обработка должна происходить в месте возникновения panic. Это возлагает на разработчика ответственность за обработку ошибки и побуждает более глубоко задуматься о том, где, кем и как эта ошибка должна обрабатываться. Таким образом, пользователю предоставляется автономия, но при этом оставляется пространство для размышлений.

Кроме того, выбор самого слова panic также весьма удачен: в отличие от try-recover, конструкция panic-recover по самому своему названию заставляет разработчика использовать её только в ситуациях явной ошибки, а не исключительных ситуаций. Естественным образом разработчик не злоупотребляет recover и применяет его только там, где это действительно необходимо. Это во многом способствует написанию лаконичного кода на Go.