Error, panic и recover без суеты

Ошибки в Go сначала выглядят многословно: почти после каждого важного действия появляется if err != nil. Но именно в этом и смысл подхода: ошибка не прячется в исключении, а идет обычным возвращаемым значением. В этом уроке разберем error, fmt.Errorf с %w, errors.Is, errors.As, а также спокойное правило: panic не заменяет обработку ошибок

Цель простая: читать Go-код без ощущения, что ошибки занимают половину экрана зря

Что получится в конце

Мы напишем функцию, которая ищет задачу по ID:

task, err := findTask(42)
if err != nil {
    if errors.Is(err, ErrNotFound) {
        fmt.Println("задача не найдена")
        return
    }

    fmt.Println("внутренняя ошибка:", err)
    return
}

fmt.Println(task.Title)

Такой код похож на реальный backend: есть нормальная ошибка «не найдено» и есть остальные ошибки, которые нужно логировать и расследовать

error как обычное значение

В Go error — это интерфейс. Упрощенно он выглядит так:

type error interface {
    Error() string
}

Если функция может не выполнить задачу, она часто возвращает результат и error:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }

    return a / b, nil
}

Вызов:

result, err := divide(10, 0)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

fmt.Println(result)

nil означает, что ошибки нет. Это базовая идиома Go: сначала проверили ошибку, потом работаем с результатом

Своя sentinel error

Создайте проект:

mkdir go-errors
cd go-errors
go mod init example.com/go-errors

Файл main.go:

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("task not found")

type Task struct {
    ID    int
    Title string
}

func findTask(id int) (Task, error) {
    tasks := []Task{
        {ID: 1, Title: "Разобрать error в Go"},
    }

    for _, task := range tasks {
        if task.ID == id {
            return task, nil
        }
    }

    return Task{}, ErrNotFound
}

func main() {
    task, err := findTask(42)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            fmt.Println("задача не найдена")
            return
        }

        fmt.Println("ошибка:", err)
        return
    }

    fmt.Println(task.Title)
}

Запустите:

go run .

Вывод:

задача не найдена

ErrNotFound здесь называют sentinel error: заранее известная ошибка, с которой можно сравнивать другие ошибки

Зачем нужен errors.Is

Можно было бы написать:

if err == ErrNotFound

Но в реальных проектах ошибку часто оборачивают контекстом. Тогда прямое сравнение перестает работать, а errors.Is продолжает понимать цепочку

Изменим findTask:

return Task{}, fmt.Errorf("find task %d: %w", id, ErrNotFound)

Теперь текст ошибки стал подробнее:

find task 42: task not found

Но проверка остается рабочей:

if errors.Is(err, ErrNotFound) {
    fmt.Println("задача не найдена")
    return
}

Символ %w важен. Он не просто вставляет текст ошибки, а оборачивает ее так, чтобы errors.Is и errors.As могли пройти по цепочке

errors.As и типизированные ошибки

Иногда нужно достать не конкретную sentinel error, а ошибку определенного типа

type ValidationError struct {
    Field string
    Msg   string
}

func (e ValidationError) Error() string {
    return e.Field + ": " + e.Msg
}

Функция:

func validateTitle(title string) error {
    if title == "" {
        return ValidationError{Field: "title", Msg: "must not be empty"}
    }

    return nil
}

Проверка:

err := validateTitle("")
if err != nil {
    var validationErr ValidationError
    if errors.As(err, &validationErr) {
        fmt.Println("поле:", validationErr.Field)
        fmt.Println("сообщение:", validationErr.Msg)
        return
    }
}

errors.As полезен, когда ошибка несет данные: поле формы, код внешнего сервиса, путь к файлу, статус

Когда возвращать ошибку, а когда panic

Для обычных ожидаемых проблем возвращайте error: файл не найден, пользователь ввел пустое поле, база недоступна, внешний API вернул ошибку

panic оставляют для ситуаций, где программа оказалась в невозможном или опасном состоянии: нарушен внутренний инвариант, критически неправильная конфигурация, ошибка программиста, которую нельзя нормально обработать в текущем месте

Плохой стиль:

if err != nil {
    panic(err)
}

внутри обычного HTTP handler или функции чтения данных. Такой код превращает нормальную ошибку в аварийное завершение потока выполнения

recover без героизма

recover может перехватить panic внутри deferred-функции:

func safeRun() {
    defer func() {
        if value := recover(); value != nil {
            fmt.Println("panic recovered:", value)
        }
    }()

    panic("something broke")
}

Но это не инструмент для повседневной бизнес-логики. В web-сервере recover обычно живет в middleware, чтобы один неожиданный panic не уронил весь процесс обработки запроса. Внутри обычных функций лучше возвращать error

Как читать цепочку ошибок

Хорошая ошибка отвечает на вопрос «что делали, когда сломалось»:

return fmt.Errorf("load config: %w", err)

Еще выше:

return fmt.Errorf("start server: %w", err)

В логе получится цепочка:

start server: load config: open config.yaml: no such file or directory

Это намного полезнее, чем просто no such file or directory, потому что видно контекст

Частые ошибки

Проверка ошибки пропущена. Если функция возвращает err, почти всегда его нужно проверить сразу. Игнорирование через _ должно быть осознанным исключением

Использовали %v вместо %w. %v добавит текст, но не сохранит цепочку для errors.Is и errors.As

panic вместо return err. Для ожидаемой проблемы это плохой обмен: программа теряет управляемость

Слишком общий текст ошибки. failed или error happened не помогают. Добавляйте действие: connect database, decode request, save task

Что может быть еще интересно по этой теме

Почему в Go так много if err != nil? Потому что ошибка является частью обычного потока программы. Это явно, иногда шумно, зато хорошо читается при отладке

Нужно ли логировать ошибку на каждом уровне? Обычно нет. Часто лучше добавить контекст через %w, вернуть выше и залогировать один раз на границе приложения

Можно ли делать свои типы ошибок? Да, если вместе с ошибкой нужны данные. Если нужен только текст, хватит errors.New или fmt.Errorf

Что возвращать из HTTP API при ошибке? Наружу отдавайте понятный статус и безопасное сообщение, а подробную внутреннюю ошибку пишите в лог

Что открыть дальше

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x