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" не имеет расширенияПолезное
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
