Краткий справочник по обработке ошибок в Go. Всё на одной странице: создание, оборачивание, проверка, выбор инструмента под ситуацию. Плюс антипаттерны, которые стоит ловить на ревью. Здесь только «как». Объяснение «почему так» есть в полной статье про обработку ошибок. Если вы только начинаете изучать язык, весь маршрут есть в дорожной карте Go для начинающих.
Создание ошибок
// Статическое сообщение
err := errors.New("файл не найден")
// Сообщение с параметрами
err := fmt.Errorf("файл %s не найден", filename)
// Sentinel-ошибка уровня пакета
var ErrNotFound = errors.New("not found")
// Типизированная ошибка с данными
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("валидация поля %s: %s", e.Field, e.Message)
}
Оборачивание: %w и %v
// %w оборачивает: оригинал сохраняется, errors.Is его найдёт
return fmt.Errorf("чтение конфига: %w", err)
// %v форматирует: текст останется, связь с оригиналом потеряется
return fmt.Errorf("чтение конфига: %v", err)
| Глагол | Связь с оригиналом | Когда использовать |
|---|---|---|
%w | Сохраняется, работают errors.Is/errors.As | По умолчанию |
%v | Теряется | Скрыть деталь реализации от вызывающего кода |
Контекст в обёртке отвечает на вопросы «что делали» и «с какими данными»:
return fmt.Errorf("получение пользователя id=%d: %w", id, err)
Проверка: errors.Is и errors.As
// errors.Is: проверка на конкретное значение (sentinel)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
// errors.As: извлечение типизированной ошибки
var ve *ValidationError
if errors.As(err, &ve) {
log.Printf("поле %s: %s", ve.Field, ve.Message)
}
Обе функции рекурсивно проходят всю цепочку обёрток. Прямое сравнение err == sql.ErrNoRows сломается на первой же обёртке через %w. Не используйте его.
Несколько ошибок: errors.Join
// Собрать все ошибки валидации, а не падать на первой
func validate(o Order) error {
var errs []error
if o.CustomerID == 0 {
errs = append(errs, errors.New("не указан customer_id"))
}
if len(o.Items) == 0 {
errs = append(errs, errors.New("заказ без товаров"))
}
return errors.Join(errs...) // nil, если все ошибки nil
}
// Закрыть несколько ресурсов одним выражением
func (app *App) Close() error {
return errors.Join(app.db.Close(), app.cache.Close())
}
errors.Is и errors.As работают и с объединёнными ошибками: проверяется каждая ошибка в группе.
Ситуация → инструмент
| Ситуация | Инструмент |
|---|---|
| Создать ошибку со статичным текстом | errors.New |
| Создать ошибку с параметрами | fmt.Errorf |
| Добавить контекст и сохранить оригинал | fmt.Errorf с %w |
| Проверить «та ли это ошибка» | errors.Is |
| Достать данные из ошибки | errors.As + типизированная ошибка |
| Собрать несколько ошибок в одну | errors.Join |
| Ошибки из группы горутин | errgroup (отдельный разбор) |
| Программа не может продолжать работу | panic (только инициализация и нарушение инвариантов) |
Sentinel или типизированная ошибка
| Критерий | Sentinel (var ErrX = errors.New) | Типизированная (struct) |
|---|---|---|
| Что важно вызывающему | Сам факт: «не найдено», «нет доступа» | Данные: какое поле, какой лимит |
| Проверка | errors.Is | errors.As |
| Пример из stdlib | sql.ErrNoRows, io.EOF | *os.PathError, *json.SyntaxError |
Маппинг ошибок в HTTP-ответ
func handleError(w http.ResponseWriter, err error) {
var ve *ValidationError
if errors.As(err, &ve) {
http.Error(w, ve.Error(), http.StatusBadRequest)
return
}
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
log.Printf("внутренняя ошибка: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
Антипаттерны
| Плохо | Почему | Как надо |
|---|---|---|
err == sql.ErrNoRows | Ломается при оборачивании | errors.Is(err, sql.ErrNoRows) |
strings.Contains(err.Error(), "not found") | Текст ошибки может измениться | errors.Is / errors.As |
return err без контекста | В логах непонятно, откуда ошибка | fmt.Errorf("операция X: %w", err) |
| Залогировать и вернуть ошибку | Одна ошибка в логах несколько раз | Либо обработать, либо передать выше |
json.Unmarshal(data, &v) без проверки | Тихий баг | Проверять; намеренное игнорирование показывать как _ = |
panic(err) в бизнес-логике | net/http панику восстановит, но соединение оборвётся. Паника в горутине убьёт весь процесс | Возвращать error |
| Вернуть и значение, и ошибку | Вызывающий может использовать частичный результат | При ошибке возвращать нулевое значение. Исключение: осмысленный частичный результат, как у io.Reader |
Непроверенные ошибки ловит линтер errcheck. Он входит в golangci-lint. Включите его в CI.
Тестирование ошибок
// Sentinel: проверяем через errors.Is
if !errors.Is(err, ErrNotFound) {
t.Errorf("ожидали ErrNotFound, получили: %v", err)
}
// Типизированная: извлекаем через errors.As
var ve *ValidationError
if !errors.As(err, &ve) {
t.Fatalf("ожидали ValidationError, получили: %v", err)
}
Текст ошибки через err.Error() в тестах не проверяйте. Проверяйте тип и семантику.
Итог
Минимальный рабочий набор: fmt.Errorf с %w для оборачивания, errors.Is для sentinel-ошибок, errors.As для типизированных, errors.Join для групп. Остальное в таблице «ситуация → инструмент» выше. Если по какому-то пункту хочется понять «почему именно так», открывайте полный разбор обработки ошибок.
Сохраните шпаргалку себе. Новые шпаргалки и разборы я анонсирую в Telegram-канале «Бруяко: код и команда».
Теги: