Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
В 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
стала бы короче и легче для чтения.
Реализация исключений через панику
Конечный результат будет выглядеть так:
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
и передает в нашу функцию обработчик.
Подход имитирует обработку исключений в Go. Функцию Try
можно модифицировать таким образом, чтобы она принимала много отдельных обработчиков ошибок и сама вызывала нужный, хотя реализовать это довольно сложно. Для удобства и возможности повторного использования, функцию Try
стоит вынести в отдельный пакет и импортировать через dot import: например, import . "exception/try"
.
Теперь у нас есть возможность использовать подход обработки исключений, но так ли все хорошо на самом деле? Обработка исключений имеет и свои минусы, хотя она отделена от логики программы и позволяет писать более короткий код.
Нужны ли в 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()
}
Комментарии