Golang: Ошибки
В Go есть два способа сообщать о проблемах: ошибки и паника.
Ошибки vs исключения
Во многих языках, например, в Python или JavaScript, при возникновении ошибки выбрасывается исключение (exception), которое можно перехватить и обработать. В Go всё иначе: вместо исключений используется явная передача ошибок через возвращаемые значения.
Такой подход делает поток исполнения более прозрачным: вы всегда видите, где может произойти ошибка и как она обрабатывается. Цена за это - постоянные проверки при вызове функций, которые могут вернуть ошибку.
Паника — это ошибки программиста
Go предоставляет механизм паники (panic), но он используется редко. Паника означает, что программа не может продолжать выполнение. Это обычно связано с ошибками в логике программы: выход за границы массива, обращение к nil
, нарушение внутренних инвариантов.
Пример паники:
denominator := 0
result := 10 / denominator // Деление на ноль — вызывает панику
fmt.Println("Результат:", result)
Панику можно вызвать явно с помощью функции panic()
. Обычно это делается, когда программа сталкивается с ситуацией, которая не должна происходить при корректной логике, и продолжать выполнение небезопасно:
func divide(a, b int) int {
if b == 0 {
panic("деление на ноль недопустимо")
}
return a / b
}
Паника в Go приводит к аварийному завершению программы: выполнение прерывается, и если не задействован специальный механизм для её обработки, программа прекращает работу и выводит сообщение об ошибке вместе со стек-трейсом.
Обработка ошибок
В Go есть специальный тип error
, который можно использовать для возврата сообщения об ошибке из функции. Это встроенный тип в стандартной библиотеке, и его удобно использовать для обработки любых проблем, которые могут возникнуть при выполнении функции.
Сигнатура функции, которая может вернуть ошибку, обычно выглядит так:
func имя(аргументы) (результат, error)
Функция divide()
ниже возвращает два значения: результат деления и ошибку. Если деление прошло успешно, ошибка равна nil
. Если произошла ошибка (например, деление на ноль), вместо результата возвращается 0 и ненулевая ошибка. Вот пример безопасного деления с проверкой деления на ноль:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль невозможно")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Результат:", result)
}
Что здесь происходит
- Функция
divide()
возвращает два значения: результат и ошибку. - Если делитель равен нулю, создаётся ошибка с помощью
errors.New
. - В
main()
мы проверяем: еслиerr
неnil
, значит произошла ошибка — выводим её.
Более подробное сообщение об ошибке
Для создания более понятных сообщений можно использовать fmt.Errorf()
:
return 0, fmt.Errorf("ошибка деления %v / %v: делитель равен нулю", a, b)
Пример: нормальное и ошибочное деление
func main() {
fmt.Println(divide(12, 4)) // 3 <nil>
fmt.Println(divide(5, 0)) // 0 ошибка деления 5 / 0: делитель равен нулю
}
Как устроены программы в Go с ошибками
Так как в Go нет исключений и нет конструкции try/catch
, весь контроль за ошибками осуществляется через возврат значения типа error
. Это приводит к тому, что код часто выглядит как последовательность:
результат, ошибка := функция()
if ошибка != nil {
// Обработка ошибки
return ...
}
Такая структура повторяется много раз подряд, что может создать матрешку из вложенных проверок. Поэтому работая с ошибками, принято их выпрямлять:
func process() error {
data, err := readData()
if err != nil {
return err
}
value, err := parseData(data)
if err != nil {
return err
}
result, err := computeResult(value)
if err != nil {
return err
}
err = saveResult(result)
if err != nil {
return err
}
fmt.Println("Обработка завершена успешно")
return nil
}
Задание
Реализуйте функцию GetFileExtension(filename string) (string, error)
, которая возвращает расширение файла (текст после последней точки). Если в имени файла нет точки, функция должна вернуть ошибку.
Примеры
ext, err := GetFileExtension("photo.jpeg")
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Расширение файла:", ext)
}
// => Расширение файла: jpeg
ext, err = GetFileExtension("backup.zip")
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Расширение файла:", ext)
}
// => Расширение файла: zip
ext, err = GetFileExtension("LICENSE")
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Расширение файла:", ext)
}
// => Ошибка: файл "LICENSE" не имеет расширения
Полезное
Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект
Если вы зашли в тупик, то самое время поговорить с нашим асситентом Тота во вкладке "ИИ-помощник":
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи. В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в обратной связи нашего сообщества
Ваше упражнение проверяется по этим тестам
package solution
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetFileExtension(t *testing.T) {
tests := []struct {
name string
filename string
wantExt string
shouldFail bool
}{
{"Simple extension", "file.txt", "txt", false},
{"Multiple dots", "archive.tar.gz", "gz", false},
{"No extension", "README", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ext, err := GetFileExtension(tt.filename)
if tt.shouldFail {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantExt, ext)
}
})
}
}
Решение учителя откроется через:
20:00
