Обработка ошибок в Go: try/catch и не только от А до Я

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

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

Начать курс

Обработка ошибок в Go: философия и практика

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

1. Основы: ошибки — это просто значения

В Go ошибка — это интерфейс error с единственным методом Error() string. Любая функция, которая может завершиться неудачей, возвращает ошибку как последнее возвращаемое значение.

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

Ключевой момент: проверка if err != nil — это стандартный способ обработки ошибок в Go. Нет скрытых исключений, которые могут «вылететь» неожиданно.

2. Создание и кастомизация ошибок

Go предоставляет несколько способов создания ошибок:

  • errors.New() — простая текстовая ошибка.
  • fmt.Errorf() — ошибка с форматированием (поддерживает %w для 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
}

func main() {
    _, err := safeDivide(10, 0)
    if err != nil {
        // Type assertion для получения дополнительных полей
        var divErr *DivisionError
        if errors.As(err, &divErr) {
            fmt.Printf("Ошибка с %f на %f: %s\n", divErr.Dividend, divErr.Divisor, divErr.Message)
        }
    }
}

Использование errors.As() позволяет проверить, является ли ошибка определенным типом, и извлечь из нее данные. Это аналог instanceof в других языках.

3. Паттерны обработки ошибок: от простого к сложному

В Go существует несколько устоявшихся паттернов работы с ошибками:

3.1. Sentinel errors (ошибки-маркеры)

Это предопределенные ошибки, которые можно сравнивать с помощью ==. Пример: io.EOF.

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("элемент не найден")

func findUser(id int) (string, error) {
    if id != 1 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

func main() {
    _, err := findUser(2)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Пользователь не найден, создаем нового...")
    }
}

3.2. Wrapping ошибок (обертывание)

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

func readConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("чтение конфига %s: %w", path, err)
    }
    // ... обработка data
    return nil
}

func main() {
    err := readConfig("/nonexistent/config.json")
    if err != nil {
        // Проверка, есть ли в цепочке os.ErrNotExist
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Файл не существует:", err)
        } else {
            fmt.Println("Другая ошибка:", err)
        }
    }
}

3.3. Паника и восстановление (panic/recover)

Хотя panic в Go не является заменой try/catch, он используется для действительно исключительных ситуаций (например, ошибки программиста). recover позволяет перехватить панику.

func safeCall(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("паника: %v", r)
        }
    }()
    fn()
    return nil
}

func main() {
    err := safeCall(func() {
        panic("что-то пошло не так")
    })
    if err != nil {
        fmt.Println("Перехвачено:", err)
    }
}

Важно: panic/recover следует использовать только в критических сценариях (например, для восстановления после паники в горутинах или HTTP-обработчиках). В обычной логике всегда используйте возврат ошибок.

4. Best practices обработки ошибок в Go

Чтобы ваш код на Go был надежным и читаемым, следуйте этим рекомендациям:

  • Всегда проверяйте ошибки. Игнорирование err через _ допустимо только если вы уверены в успехе (например, fmt.Fprint).
  • Не используйте панику как try/catch. Паника предназначена для ошибок, которые не должны происходить (выход за границы массива, nil pointer).
  • Оборачивайте ошибки с контекстом. Вместо простого return err используйте fmt.Errorf("что-то делали: %w", err).
  • Используйте errors.Is и errors.As для проверки цепочек ошибок, а не прямое сравнение.
  • Избегайте дублирования. Если одна и та же ошибка проверяется в нескольких местах, вынесите логику в функцию-

Блоги

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