Обработка ошибок в Go: try/catch и не только — полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Go: философия без try/catch

В мире Go (Golang) нет привычного для многих разработчиков механизма try/catch. Это не случайность, а осознанное архитектурное решение. Создатели языка посчитали, что явная обработка ошибок делает код более надежным и читаемым. Вместо того чтобы прятать исключения в стеке вызовов, Go предлагает программисту всегда проверять результат каждой потенциально опасной операции. В этой статье мы разберем все основные подходы к обработке ошибок в Go: от базовых error до мощных panic и recover, а также покажем, как написать код, который не упадет в самый неподходящий момент.

1. Базовый механизм: интерфейс error

Основа обработки ошибок в Go — встроенный интерфейс error. Он имеет всего один метод:

type error interface {
    Error() string
}

Любая функция, которая может завершиться неудачей, должна возвращать error в качестве последнего значения. Принято возвращать nil, если ошибки нет, и ненулевое значение — если произошел сбой.

Пример базовой обработки:

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

Это и есть аналог try/catch в Go — явная проверка if err != nil. Такой подход заставляет разработчика думать об ошибках на каждом шагу.

2. Кастомные типы ошибок и обёртывание (wrapping)

Часто недостаточно просто вернуть строку с описанием. Нужно передать контекст: где произошла ошибка, какие параметры были переданы. Для этого создают собственные типы ошибок.

Создание кастомной ошибки:

type DivisionError struct {
    Dividend float64
    Divisor  float64
    Message  string
}

func (e *DivisionError) Error() string {
    return fmt.Sprintf("ошибка деления: %s (делимое: %f, делитель: %f)", e.Message, e.Dividend, e.Divisor)
}

func safeDivide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, &DivisionError{Dividend: a, Divisor: b, Message: "делитель равен нулю"}
    }
    return a / b, nil
}

Обёртывание ошибок (wrapping):
Начиная с Go 1.13, появилась возможность оборачивать одну ошибку в другую, добавляя контекст:

import "fmt"

func readConfig(path string) error {
    file, err := openFile(path)
    if err != nil {
        return fmt.Errorf("не удалось прочитать конфиг %s: %w", path, err)
    }
    defer file.Close()
    // ... чтение файла
    return nil
}

Директива %w создает цепочку ошибок. Позже вы можете проверить, является ли ошибка конкретным типом, с помощью errors.Is() и errors.As().

3. Panic и Recover: аварийные ситуации

Иногда происходит нечто, что не должно случиться в принципе — выход за границы массива, разыменование нулевого указателя. В таких случаях Go использует panic. Это не замена try/catch, а механизм для критических, невосстанавливаемых ошибок.

Пример panic:

func main() {
    arr := []int{1, 2, 3}
    fmt.Println(arr[10]) // panic: runtime error: index out of range
}

Однако, если вы хотите "поймать" панику и восстановить выполнение, используется recover в паре с defer. Это ближайший аналог catch.

Пример recover:

func safeCall(f func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Восстановлено после паники:", r)
        }
    }()
    f() // если здесь произойдет panic, мы её перехватим
}

func main() {
    safeCall(func() {
        panic("что-то пошло не так!")
    })
    fmt.Println("Программа продолжает работу...")
}

Важно: recover работает только внутри отложенной функции (defer). Используйте его только для логирования и корректного завершения, а не для восстановления логики бизнес-процессов.

4. Defer: гарантированное выполнение

defer — это мощный инструмент, который часто используется вместе с обработкой ошибок. Он откладывает выполнение функции до момента выхода из текущей функции. Это идеальный способ закрыть файл, освободить ресурс или зафиксировать ошибку.

Пример с defer и ошибками:

func copyFile(src, dst string) (err error) {
    source, err := os.Open(src)
    if err != nil {
        return err
    }
    defer func() {
        if closeErr := source.Close(); closeErr != nil && err == nil {
            err = closeErr // если ошибка закрытия важнее
        }
    }()

    destination, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer func() {
        if closeErr := destination.Close(); closeErr != nil && err == nil {
            err = closeErr
        }
    }()

    _, err = io.Copy(destination, source)
    return err
}

Обратите внимание: мы используем именованный возврат (err error), чтобы в defer можно было изменить возвращаемое значение. Это позволяет гарантировать закрытие файлов даже при возникновении ошибки.

5. Лучшие практики и сравнение с try/catch

Многие новички в Go жалуются на обилие if err != nil. Однако это сознательная жертва в пользу прозрачности. Вот несколько советов, как сделать код чище:

  • Не игнорируйте ошибки. Если функция возвращает error, всегда проверяйте его. Исключение — fmt.Println и подобные, где ошибка маловероятна.
  • Группируйте проверки. Если в функции много вызовов, возвращающих ошибки, рассмотри

Блоги

Рекомендаций по книгам