Как в Go принято обрабатывать и возвращать ошибки?

Ответ

В Go используется идиоматический подход к обработке ошибок, основанный на явной проверке и возврате значений типа error.

Основной принцип: функции, которые могут завершиться с ошибкой, возвращают её в качестве последнего значения в кортеже. Успешное выполнение обозначается возвратом nil в этой позиции.

func divide(a, b float64) (float64, error) {
    if b == 0 {
        // Используем fmt.Errorf для создания форматированной ошибки
        return 0, fmt.Errorf("деление на ноль: делимое = %.2f", a)
    }
    return a / b, nil // Ошибки нет, возвращаем nil
}

// Вызов и проверка
result, err := divide(10, 0)
if err != nil {
    // Ошибка произошла, обрабатываем её
    fmt.Println("Произошла ошибка:", err)
}

Способы создания ошибок:

  1. errors.New(): Создает простую ошибку с фиксированным текстовым сообщением. Идеально для статических ошибок.
    var ErrDivisionByZero = errors.New("деление на ноль")
  2. fmt.Errorf(): Создает ошибку с форматированием, как fmt.Sprintf. Позволяет добавлять в сообщение контекст (например, значения переменных).

Обертывание ошибок (Error Wrapping)

Начиная с Go 1.13, появилась возможность "оборачивать" ошибки, чтобы сохранить исходную причину. Это делается с помощью директивы %w в fmt.Errorf.

func readFile() error {
    err := openFile()
    if err != nil {
        // Оборачиваем исходную ошибку, добавляя контекст
        return fmt.Errorf("не удалось прочитать файл: %w", err)
    }
    return nil
}

Проверка ошибок

Для проверки "обернутых" ошибок используются функции из пакета errors:

  • errors.Is(err, target): Проверяет, есть ли в цепочке ошибок err ошибка, равная target. Используется для сравнения с конкретными экземплярами ошибок (например, io.EOF).
  • errors.As(err, &target): Проверяет, есть ли в цепочке err ошибка, которую можно присвоить переменной target. Используется для работы с кастомными типами ошибок.

Пример с кастомным типом:

type MyError struct {
    Code int
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("код: %d, сообщение: %s", e.Code, e.Msg)
}

func doSomething() error {
    return &MyError{Code: 500, Msg: "внутренняя ошибка сервера"}
}

func main() {
    err := doSomething()
    var myErr *MyError
    if errors.As(err, &myErr) {
        fmt.Printf("Перехвачена кастомная ошибка. Код: %dn", myErr.Code)
    }
}