Share This
Связаться со мной
Крути в низ
Categories
//🐛 Исключения в Go – это легко?

🐛 Исключения в Go – это легко?

В Go (Golang) нет специального механизма обработки исключений, и создатели языка не собираются его добавлять. Попробуем разобраться, хорошо это или плохо и как лучше разрешать проблемные ситуации в приложениях. Обсудить

iskljuchenija v go eto legko d7e9d86 - 🐛 Исключения в 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).
  • Функция прерывает нормальное выполнение и возвращает ошибку как значение (в Go принято возвращать ее последним значением).

Если проблем не возникло, функция продолжит работу и по завершении вернет результат (если он есть) и пустое значение ошибки. Есть и другой вариант: к примеру, 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 добавить исключения

Этот подход использовать проще: при возникновении проблемной ситуации вместо явного возврата значения ошибки из функции бросается исключение. Оно неявно возвращается вверх по стеку вызовов функций и может быть обработано в специальном блоке. Если блок обработки исключения не найден, программа (немного упрощенно, на самом деле – программный поток) завершается с сообщением об исключении. При возникновении исключения в него также записывается состояние стека вызовов функций.

При этом пропадает нужда в проверке на возникновение ошибки при каждом вызове функции. Если представить, что в 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 стала бы короче и легче для чтения.

Реализация исключений через панику

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

Конечный результат будет выглядеть так:

         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() }      

Механизм паники в Go очень похож на исключения. Функции неявным образом завершаются и паника идет по стеку вызовов обратно, пока программа аварийно не завершится. При этом панику можно перехватить и остановить с помощью функции recover. Более того, благодаря встроенной функции panic ее несложно и вызвать. Функция принимает один аргумент типа interface{}, который можно будет получить после вызова recover. Функция recover должна вызываться в отложенной через defer функции, так как только отложенные функции исполняются даже при панике.

Используя эти знания, мы можем поступить так: при возникновении проблемы искусственно вызывать панику, передавая ей в качестве аргумента произошедшую ошибку. Чтобы отловить эту ошибку будем использовать вспомогательную функцию Try. Первым аргументом передаем анонимную функцию с нашим кодом, а вторым – функцию обработки любой возникшей ошибки. Если функция в первом аргументе паникует, то Try перехватывает панику через recover, извлекает из возвращаемого значения тип error и передает в нашу функцию обработчик.

Подход имитирует обработку исключений в Go. Функцию Try можно модифицировать таким образом, чтобы она принимала много отдельных обработчиков ошибок и сама вызывала нужный, хотя реализовать это довольно сложно. Для удобства и возможности повторного использования, функцию Try стоит вынести в отдельный пакет и импортировать через dot import: например, import . "exception/try".

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

Нужны ли в Go исключения?

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

Из дополнительных побочных эффектов исключений стоит отметить потерю производительности. Именно поэтому разработчики игр отказываются от них в пользу альтернативных решений.

***

Писать код на основе исключений в 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

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close