Обработка ошибок
В Go существует три уровня исключений:
error: нормальная ошибка потока, требует обработки, игнорирование не приведёт к падению программыpanic: серьёзная проблема, программа должна завершиться после обработкиfatal: критическая проблема, программа должна немедленно завершиться
Точнее, в Go нет исключений — они выражаются через ошибки. В Go нет конструкций try-catch-finally. Основатель Go хочет контролировать ошибки, не требуя вложенных try-catch. Поэтому ошибки обычно возвращаются как значения функций:
func main() {
// Открытие файла
if file, err := os.Open("README.txt"); err != nil {
fmt.Println(err)
return
}
fmt.Println(file.Name())
}Код открывает файл README.txt. При неудаче возвращается ошибка, выводится сообщение. Если nil — файл открыт успешно, выводится имя.
Кажется проще, чем try-catch. Но при множестве вызовов функций код заполняется if err != nil:
func main() {
sum, err := checksum("main.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(sum)
}
func checksum(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
_, err = io.Copy(hash, file)
if err != nil {
return "", err
}
var hexSum [64]byte
sum := hash.Sum(nil)
hex.Encode(hexSum[:], sum)
return string(hexSum[:]), nil
}Это критикуется в Go. Исходный код Go содержит много if err != nil. Rust также возвращает ошибки, но никто не критикует — он использует синтаксический сахар. В Go синтаксического сахара почти нет.
Но нужно рассматривать диалектически. Преимущества обработки ошибок Go:
- Малая ментальная нагрузка: есть ошибка — обработать, нет — вернуть
- Читаемость: обработка проста, код легко понять
- Отладка: ошибки генерируются возвращаемыми значениями функций, можно отследить источник
Недостатки:
- Нет информации стека (требуется сторонний пакет или собственная обёртка)
- Некрасиво, много повторяющегося кода (зависит от предпочтений)
- Пользовательские ошибки объявляются через
var— это переменные, не константы (неправильно) - Проблема затенения переменных
Предложения и обсуждения обработки ошибок в сообществе продолжаются с момента создания Go. Шутка: если вы принимаете обработку ошибок Go, вы квалифицированный Gopher.
TIP
Две статьи команды Go об обработке ошибок:
error
error — нормальная ошибка потока, её появление допустимо. В большинстве случаев следует обработать, можно игнорировать. Серьёзность error недостаточна для остановки программы. error — предопределённый интерфейс с одним методом Error(), возвращающим строку для вывода ошибки.
type error interface {
Error() string
}История error имела изменения. В версии 1.13 команда Go представила цепочечные ошибки и улучшенный механизм проверки.
Создание
Создание error:
- Использование функции
Newпакетаerrors:
err := errors.New("это ошибка")- Использование функции
Errorfпакетаfmtдля форматированной ошибки:
err := fmt.Errorf("это ошибка с %d параметром", 1)Полный пример:
func sumPositive(i, j int) (int, error) {
if i <= 0 || j <= 0 {
return -1, errors.New("должно быть положительное целое")
}
return i + j, nil
}Для лучшей поддерживаемости обычно не создают error временно, а используют как глобальные переменные. Пример из файла os/errors.go:
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)Все определены через var.
Пользовательские ошибки
Реализация метода Error() позволяет легко создать пользовательский error. Например, errorString в пакете errors:
func New(text string) error {
return &errorString{text}
}
// структура errorString
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}Поскольку errorString слишком проста для выражения, многие библиотеки и официальные пакеты создают пользовательские error для различных ошибок.
Передача
Иногда вызываемая функция возвращает ошибку, но вызывающий не обрабатывает её, а возвращает вышестоящему вызывающему. Это называется передачей. Ошибки могут упаковываться多层. При определении типа ошибки верхний вызывающий может не распознать или ошибочно определить тип. Цепочные ошибки решают эту проблему.
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}wrapError реализует интерфейс error и добавляет метод Unwrap, возвращающий ссылку на исходную ошибку. Многослойная упаковка формирует цепочку ошибок. Поскольку структура не экспортируется, используется fmt.Errorf:
err := errors.New("это исходная ошибка")
wrapErr := fmt.Errorf("ошибка, %w", err)Требуется форматный глагол %w, параметр — один допустимый error.
Обработка
Последний шаг обработки ошибок — проверка. Пакет errors предоставляет удобные функции.
func Unwrap(err error) errorerrors.Unwrap() распаковывает цепочку ошибок:
func Unwrap(err error) error {
u, ok := err.(interface { // утверждение типа
Unwrap() error
})
if !ok { // нет реализации — базовый error
return nil
}
return u.Unwrap() // вызов Unwrap
}После распаковки возвращается обёрнутая ошибка, которая может быть цепочкой. Для поиска значения или типа в цепочке используется рекурсия. Стандартная библиотека предоставляет готовые функции.
func Is(err, target error) boolerrors.Is проверяет наличие указанной ошибки в цепочке:
var originalErr = errors.New("this is an error")
func wrap1() error { // упаковка исходной ошибки
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // исходная ошибка
return originalErr
}
func main() {
err := wrap1()
if errors.Is(err, originalErr) // if err == originalErr будет false
fmt.Println("original")
}
}При проверке ошибок не следует использовать ==, лучше errors.Is().
func As(err error, target any) boolerrors.As ищет первое совпадение типа в цепочке и присваивает значение target. Иногда нужно преобразовать error в конкретный тип для деталей. Утверждение типа для цепочки неэффективно, так как исходная ошибка обёрнута. Поэтому нужна функция As:
type TimeError struct { // пользовательский error
Msg string
Time time.Time // время возникновения ошибки
}
func (m TimeError) Error() string {
return m.Msg
}
func NewMyError(msg string) error {
return &TimeError{
Msg: msg,
Time: time.Now(),
}
}
func wrap1() error { // упаковка исходной ошибки
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // исходная ошибка
return NewMyError("original error")
}
func main() {
var myerr *TimeError
err := wrap1()
// Проверка наличия *TimeError в цепочке
if errors.As(err, &myerr) { // вывод времени TimeError
fmt.Println("original", myerr.Time)
}
}target должен быть указателем на error. Поскольку структура возвращает указатель, error — тип *TimeError, target — **TimeError.
Официальный пакет errors недостаточен — нет информации стека. Рекомендуется усиленный пакет:
github.com/pkg/errorsПример:
import (
"fmt"
"github.com/pkg/errors"
)
func Do() error {
return errors.New("error")
}
func main() {
if err := Do(); err != nil {
fmt.Printf("%+v", err)
}
}Вывод:
some unexpected error happened
main.Do
D:/WorkSpace/Code/GoLeran/golearn/main.go:9
main.main
D:/WorkSpace/Code/GoLeran/golearn/main.go:13
runtime.main
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
runtime.goexit
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650Форматированный вывод показывает стек. По умолчанию стек не выводится. Пакет — усиленная версия стандартного errors, также написан официально, но не включён в стандартную библиотеку.
panic
panic (паника) — серьёзная проблема программы, требующая немедленной остановки. Иначе программа завершится с выводом стека. panic — выражение исключений времени выполнения в Go, появляется при опасных операциях для предотвращения серьёзных последствий. Перед выходом panic выполняет завершение работы, может быть восстановлен для продолжения работы.
Пример записи в nil map вызывает panic:
func main() {
var dic map[string]int
dic["a"] = 'a'
}panic: assignment to entry in nil mapTIP
При наличии нескольких горутин, если любая горутина вызывает panic без восстановления, вся программа завершится.
Создание
Явное создание panic просто — встроенная функция panic:
func panic(v any)panic принимает параметр v типа any, который выводится при стеке. Пример:
func main() {
initDataBase("", 0)
}
func initDataBase(host string, port int) {
if len(host) == 0 || port == 0 {
panic("недопустимые параметры подключения")
}
// ... другая логика
}При неудаче подключения к базе данных программа не должна запускаться. Следует выбросить panic:
panic: недопустимые параметры подключенияЗавершение работы
Перед выходом из-за panic выполняются завершающие работы, например defer:
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("panic")
defer fmt.Println("D")
}Вывод:
C
B
A
panic: panicdefer вышестоящих функций также выполняются:
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panic")
defer fmt.Println(3)
}Вывод:
C
2
1
B
A
panic: panicdefer может содержать вложенный panic:
func main() {
defer fmt.Println("A")
defer func() {
func() {
panic("panicA")
defer fmt.Println("E")
}()
}()
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}Порядок выполнения вложенного panic в defer одинаков. При panic последующая логика не выполняется.
C
2
1
A
panic: panicB
panic: panicAПри panic функция немедленно завершается, выполняются завершающие работы (defer), затем ошибка поднимается выше, вышестоящая функция выполняет завершающие работы до остановки программы.
При panic в дочерней горутине завершающие работы текущей горутины не触发. Если panic не восстановлен до выхода горутины, программа завершится.
var waitGroup sync.WaitGroup
func main() {
demo()
}
func demo() {
waitGroup.Add(1)
defer func() {
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
waitGroup.Wait() // родительская горутина ждёт дочернюю
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
waitGroup.Done()
}Вывод:
C
2
1
panic: panicBdefer в demo() не выполнены, программа завершилась. Без waitGroup для блокировки родительской горутины выполнение demo() может быть быстрее дочерней, вывод будет запутанным:
func main() {
demo()
}
func demo() {
defer func() {
// завершающие работы родительской горутины занимают 20ms
time.Sleep(time.Millisecond * 20)
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
// дочерняя горутина выполняет логику, занимает 1ms
time.Sleep(time.Millisecond)
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}Вывод:
C
D
2
1
panic: panicBВ этом примере при panic дочерней горутины родительская завершила функцию, вошла в завершающие работы, при выполнении последнего defer произошла паника дочерней, программа завершилась.
Восстановление
При panic встроенная функция recover() обрабатывает и обеспечивает продолжение работы. Должна использоваться в defer:
func main() {
dangerOp()
fmt.Println("программа нормально завершилась")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic восстановлен")
}
}()
panic("произошёл panic")
}Вызывающий не знает о panic внутри dangerOp(), программа выполняет остальную логику и нормально завершается:
произошёл panic
panic восстановлен
программа нормально завершиласьНо recover() имеет скрытые ловушки. Например, использование recover во вложенном замыкании defer:
func main() {
dangerOp()
fmt.Println("программа нормально завершилась")
}
func dangerOp() {
defer func() {
func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic восстановлен")
}
}()
}()
panic("произошёл panic")
}Функция замыкания считается вызовом функции. panic передаётся вверх, а не вниз, поэтому замыкание не может восстановить panic:
panic: произошёл panicЕщё экстремальный случай — параметр panic() равен nil:
func main() {
dangerOp()
fmt.Println("программа нормально завершилась")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic восстановлен")
}
}()
panic(nil)
}panic восстанавливается, но ошибка не выводится:
программа нормально завершиласьОсобенности recover:
- Должен использоваться в
defer - Многократное использование восстановит только один
panic - Замыкание
recoverне восстановитpanicвнешней функции - Параметр
panicзапрещён какnil
fatal
fatal — критическая проблема, требующая немедленной остановки программы без завершающих работ. Обычно вызывается функция Exit пакета os:
func main() {
dangerOp("")
}
func dangerOp(str string) {
if len(str) == 0 {
fmt.Println("fatal")
os.Exit(1)
}
fmt.Println("нормальная логика")
}Вывод:
fatalПроблемы уровня fatal редко触发 явно, обычно пассивно.
