🐛 Исключения в Go – это легко?
В Go (Golang) нет специального механизма обработки исключений, и создатели языка не собираются его добавлять. Попробуем разобраться, хорошо это или плохо и как лучше разрешать проблемные ситуации в приложениях. Обсудить Деление на ноль, сбой сети, недостаток средств на счету пользователя для совершения оплаты – все это примеры проблемных ситуаций, требующих специальной обработки. Обработка исключений и ошибок является неотъемлемой частью программирования, но в разных языках такие ситуации могут обозначаться разными терминами и обрабатываться различными методами. В Go используется термин обработка ошибок и подход к ней серьезно отличается от практикующихся в других широко используемых языках программирования. Этот подход часто критикуют, но и хвалят его не реже. Рассмотрим базовую обработку ошибок в Go на примере функции, вычисляющей частное хранящихся в двух переменных типа Функция Если проблем не возникло, функция продолжит работу и по завершении вернет результат (если он есть) и пустое значение ошибки. Есть и другой вариант: к примеру, Когда проблема становится критичной и дальнейшее нормальное выполнение программы невозможно, используется механизм паники. В предыдущем примере паника возникнет, если делитель равен нулю. Если ничего не предпринять, приложение будет завершено. Чтобы избежать этого, нужно добавить следующую проверку: где По сути все сводится к возвращению из функций значений ошибок и последующей их проверке. Явная обработка упрощает разрешение проблемных ситуаций, но требует добавления многословного повторяющегося кода проверки на Этот подход использовать проще: при возникновении проблемной ситуации вместо явного возврата значения ошибки из функции бросается исключение. Оно неявно возвращается вверх по стеку вызовов функций и может быть обработано в специальном блоке. Если блок обработки исключения не найден, программа (немного упрощенно, на самом деле – программный поток) завершается с сообщением об исключении. При возникновении исключения в него также записывается состояние стека вызовов функций. При этом пропадает нужда в проверке на возникновение ошибки при каждом вызове функции. Если представить, что в Go когда-нибудь появятся исключения, они будут выглядеть следующим образом: Пропала проверка на возникновении ошибки. Функция где Деление на ноль в этом случае также порождает исключение. Вызывающий эту функцию код выглядел бы следующим образом: В блоке Примеры кода с “исключениями” не запустятся сейчас и, вероятнее всего, в будущем, поскольку их добавления нет в планах создателей Go. Однако мы можем попробовать имитировать механизм исключений сами. Конечный результат будет выглядеть так: Чтобы это работало, нужно также изменить функции следующим образом: И код функции Механизм паники в Go очень похож на исключения. Функции неявным образом завершаются и паника идет по стеку вызовов обратно, пока программа аварийно не завершится. При этом панику можно перехватить и остановить с помощью функции Используя эти знания, мы можем поступить так: при возникновении проблемы искусственно вызывать панику, передавая ей в качестве аргумента произошедшую ошибку. Чтобы отловить эту ошибку будем использовать вспомогательную функцию Подход имитирует обработку исключений в Go. Функцию Теперь у нас есть возможность использовать подход обработки исключений, но так ли все хорошо на самом деле? Обработка исключений имеет и свои минусы, хотя она отделена от логики программы и позволяет писать более короткий код. Основной код программы действительно стал понятнее, но обработку исключений сложнее использовать и что серьезнее – правильно обрабатывающий исключения код труднее отличить от обрабатывающего их неправильно. Разрешение проблемных ситуаций и так является тяжелым и ответственным занятием, а наш имитационный подход усложняет задачу еще больше. Разработчики Go решили, что создание надежных программ требует иной методики со своими минусами и плюсами. И не только они: альтернативы исключениям есть и в других языках программирования. Из дополнительных побочных эффектов исключений стоит отметить потерю производительности. Именно поэтому разработчики игр отказываются от них в пользу альтернативных решений. *** Писать код на основе исключений в Go в определенной мере возможно, но в сообществе это даже порицается. Пытаясь использовать такой подход, вы столкнетесь с проблемой экосистемы языка. Стандартная библиотека и сторонние пакеты используют обработку ошибок в форме возвращаемых значений, а смешение методов усложнит написание кода вдвойне. Обработка ошибок – важная часть работы программиста и какой бы метод вы бы не применяли, тщательно продумывайте поведение программы в проблемных ситуациях. Удачи! Полный листинг кода:Проверка возвращаемых ошибок
string
чисел:
func divide(a, b string) (int, error) { firstNumber, err := strconv.Atoi(a) if err != nil { return 0, fmt.Errorf("преобразовать строку %s в число: %w", a, err) } secondNumber, err := strconv.Atoi(b) if err != nil { return 0, fmt.Errorf("преобразовать строку %s в число: %w", b, err) } division := firstNumber / secondNumber return division, nil }
strconv.Atoi
конвертирует строку в целое число. Переданный ей параметр может оказаться и не числом, поэтому функция возвращает два значения: первое – результат при успешном выполнении; второе – значение ошибки, если она возникла. После вызова функции проверяется, произошла ли ошибка и если да, производятся следующие действия:fmt.Errorf
и специальной последовательности символов %w
).strconv.Itoa
преобразует число в строку и не возвращает ошибок.Механизм паники
if secondNumber == 0 { return 0, ErrZeroDivisionAttempt } division := firstNumber / secondNumber return division, nil
var ErrZeroDivisionAttempt = errors.New("divide by zero is not allowed")
err != nil
после вызова почти каждой функции или метода. Это увеличивает количество строк в исходных текстах и создает помехи в понимании основной логики кода. Обработка ошибок в такой форме вызывает негодование у привыкших к более традиционному подходу обработки исключений программистов.Если в Go добавить исключения
func divide(a, b string) int { firstNumber := strconv.Atoi(a) secondNumber := strconv.Atoi(b) division := firstNumber / secondNumber return division }
strconv.Atoi
теперь возвращает только одно значение и при проблемной ситуации вместо возврата ошибки бросает исключение с помощью оператора throw
:
func Atoi(s string) int { if s == "" { throw ErrEmptyArgument } // ... }
var ErrEmptyArgument = errors.New("empty argument")
try { result := divide("15", "10") fmt.Println(result) } catch (e ErrZeroDivisionAttempt) { fmt.Println("Делить на ноль нельзя") } catch (e ErrEmptyArgument) { fmt.Println("Невозможно конвертировать пустую строку в число") }
try
код способный породить исключение. Если исключение будет брошено, оно перехватится одним из блоков catch
и будет выполнен соответствующий типу исключения код. Если для типа исключения (или переменной в нашем случае) не найден подходящий блок catch
, исключение поднимается дальше в вызывающую функцию и далее до тех пор, пока подходящий catch
не будет найден или программа (поток) не завершится. Функция divide
стала бы короче и легче для чтения. Реализация исключений через панику
Try(func() { result := divide("15", "10") fmt.Println(result) }, func(err error) { if err == ErrZeroDivisionAttempt { fmt.Println("Делить на ноль нельзя") } else if err == ErrEmptyArgument { fmt.Println("Невозможно конвертировать пустую строку в число") } })
func divide(a, b string) int { firstNumber := Atoi(a) secondNumber := Atoi(b) if secondNumber == 0 { panic(ErrZeroDivisionAttempt) } division := firstNumber / secondNumber return division } func Atoi(s string) int { if s == "" { panic(ErrEmptyArgument) } converted, err := strconv.Atoi(s) if err != nil { panic(err) } return converted }
Try
:
var ErrNotAnError = errors.New("not an error") func Try(code func(), Catch func(err error)) { defer func() { e := recover() if e != nil { err, ok := e.(error) if !ok { err = ErrNotAnError } Catch(err) } }() code() }
recover
. Более того, благодаря встроенной функции panic
ее несложно и вызвать. Функция принимает один аргумент типа interface{}
, который можно будет получить после вызова recover
. Функция recover
должна вызываться в отложенной через defer
функции, так как только отложенные функции исполняются даже при панике. Try
. Первым аргументом передаем анонимную функцию с нашим кодом, а вторым – функцию обработки любой возникшей ошибки. Если функция в первом аргументе паникует, то Try
перехватывает панику через recover
, извлекает из возвращаемого значения тип error
и передает в нашу функцию обработчик.Try
можно модифицировать таким образом, чтобы она принимала много отдельных обработчиков ошибок и сама вызывала нужный, хотя реализовать это довольно сложно. Для удобства и возможности повторного использования, функцию Try
стоит вынести в отдельный пакет и импортировать через dot import: например, import . "exception/try"
.Нужны ли в Go исключения?
package main import ( "errors" "fmt" "strconv" ) func main() { Try(func() { result := divide("15", "0") fmt.Println(result) }, func(err error) { switch err { case ErrZeroDivisionAttempt: fmt.Println("Делить на ноль нельзя") case ErrEmptyArgument: fmt.Println("Невозможно конвертировать пустую строку в число") } }) } var ErrZeroDivisionAttempt = errors.New("divide by zero is not allowed") var ErrEmptyArgument = errors.New("empty argument") func divide(a, b string) int { firstNumber := Atoi(a) secondNumber := Atoi(b) if secondNumber == 0 { panic(ErrZeroDivisionAttempt) } division := firstNumber / secondNumber return division } func Atoi(s string) int { if s == "" { panic(ErrEmptyArgument) } converted, err := strconv.Atoi(s) if err != nil { panic(err) } return converted } var ErrNotAnError = errors.New("not an error") func Try(code func(), Catch func(err error)) { defer func() { e := recover() if e != nil { err, ok := e.(error) if !ok { err = ErrNotAnError } Catch(err) } }() code() }
- 5 views
- 0 Comment