Модуль
1Ошибки: основы2Оборачивание ошибок← вы здесь3Свои типы ошибок4panic и recover
Урок 2~12 минут

Оборачивание ошибок

Зачем оборачивать ошибки?

Представь: пользователь нажал «Сохранить», и что-то пошло не так. Ты получаешь ошибку:

permission denied

Что это? Какой файл? На каком этапе? Непонятно.

А вот хорошая ошибка:

saveProfile: writeFile: open /home/user/.config/app.json: permission denied

Каждый уровень добавил контекст. Это и есть оборачивание ошибок — каждая функция добавляет свой слой информации, сохраняя оригинальную ошибку внутри.


fmt.Errorf с %w

До Go 1.13 оборачивание требовало кастомных типов. Теперь достаточно %w:

go
// Низкий уровень: файловая операция
func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("readConfig: %w", err)
        // оборачиваем оригинальную ошибку
    }
    return data, nil
}
 
// Средний уровень: загрузка настроек
func loadSettings() (*Settings, error) {
    data, err := readConfig("/etc/app/config.json")
    if err != nil {
        return nil, fmt.Errorf("loadSettings: %w", err)
        // снова оборачиваем
    }
    // ...
}
 
// Высокий уровень: запуск приложения
func main() {
    settings, err := loadSettings()
    if err != nil {
        log.Fatal(err)
        // loadSettings: readConfig: open /etc/app/config.json: no such file or directory
    }
}

Цепочка из вызовов читается как стек трейс — сразу понятно, где и что сломалось.


errors.Is: найти ошибку в цепочке

Раньше джуны писали так:

go
// ПЛОХО: сравнение строк или прямое ==
if err.Error() == "file not found" { ... }      // хрупко
if err == os.ErrNotExist { ... }                 // сломается если err обёрнута

errors.Is раскручивает цепочку оборачиваний и ищет нужную ошибку:

go
import "errors"
 
_, err := os.Open("missing.txt")
wrapped := fmt.Errorf("loadConfig: %w", err)
doubleWrapped := fmt.Errorf("init: %w", wrapped)
 
// Без errors.Is — не работает с обёрнутыми ошибками:
fmt.Println(wrapped == os.ErrNotExist)       // false!
fmt.Println(doubleWrapped == os.ErrNotExist) // false!
 
// С errors.Is — раскручивает цепочку:
fmt.Println(errors.Is(wrapped, os.ErrNotExist))       // true
fmt.Println(errors.Is(doubleWrapped, os.ErrNotExist)) // true

Практический пример — разное поведение в зависимости от причины ошибки:

go
func openFile(path string) {
    f, err := os.Open(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("файл не существует, создаём новый")
            createDefault(path)
            return
        }
        if errors.Is(err, os.ErrPermission) {
            fmt.Println("нет прав доступа, обратитесь к администратору")
            return
        }
        fmt.Println("неожиданная ошибка:", err)
    }
    defer f.Close()
}

errors.As: извлечь тип из цепочки

errors.Is отвечает на вопрос «есть ли в цепочке эта ошибка?». errors.As — «есть ли в цепочке ошибка такого типа, и дай мне её»:

go
type ValidationError struct {
    Field   string
    Message string
}
 
func (e *ValidationError) Error() string {
    return fmt.Sprintf("поле %q: %s", e.Field, e.Message)
}
 
func validateUser(u User) error {
    if u.Age < 0 {
        return &ValidationError{Field: "age", Message: "не может быть отрицательным"}
    }
    return nil
}
 
func registerUser(u User) error {
    if err := validateUser(u); err != nil {
        return fmt.Errorf("registerUser: %w", err)
    }
    return nil
}
 
// В обработчике HTTP запроса:
err := registerUser(user)
if err != nil {
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        // получили конкретный тип — можем обратиться к полям
        http.Error(w, fmt.Sprintf("ошибка в поле %s: %s", valErr.Field, valErr.Message), 400)
        return
    }
    http.Error(w, "внутренняя ошибка", 500)
}

Это аналог instanceof + приведения типа из Java/C#, но работающий через всю цепочку оборачиваний.


%w vs %v: частая ошибка

go
err := errors.New("оригинальная ошибка")
 
// %w — оборачивает, цепочка сохраняется
wrapped := fmt.Errorf("контекст: %w", err)
fmt.Println(errors.Is(wrapped, err)) // true ✓
 
// %v — просто включает строку, цепочка ТЕРЯЕТСЯ
strOnly := fmt.Errorf("контекст: %v", err)
fmt.Println(errors.Is(strOnly, err)) // false ✗

Правило простое: если хочешь, чтобы errors.Is/errors.As работали — используй %w. Используй %v только когда тебе не нужна цепочка (например, в логах).


errors.Unwrap: ручная работа с цепочкой

Под капотом errors.Is и errors.As используют метод Unwrap():

go
wrapped := fmt.Errorf("контекст: %w", originalErr)
 
// Получить оригинальную ошибку:
inner := errors.Unwrap(wrapped)
fmt.Println(inner == originalErr) // true

Если ты создаёшь кастомный тип ошибки и хочешь, чтобы он участвовал в цепочке — реализуй Unwrap():

go
type AppError struct {
    Code int
    Err  error  // оборачиваемая ошибка
}
 
func (e *AppError) Error() string {
    return fmt.Sprintf("[%d] %v", e.Code, e.Err)
}
 
func (e *AppError) Unwrap() error {
    return e.Err  // errors.Is/As будут раскручивать через это
}

Рекомендации по оборачиванию

Добавляй контекст на каждом уровне:

go
// Хорошо: контекст добавляет информацию
return fmt.Errorf("getUserByEmail(%q): %w", email, err)
 
// Плохо: контекст ничего не говорит
return fmt.Errorf("error: %w", err)
return fmt.Errorf("failed: %w", err)

Не оборачивай дважды одно и то же:

go
// Плохо: "database" повторяется на каждом уровне
// database: database: database: connection refused

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

%w в fmt.Errorf оборачивает ошибку в цепочку. errors.Is проверяет наличие конкретной ошибки в цепочке. errors.As извлекает конкретный тип из цепочки. Это заменяет instanceof из других языков.
%w preserves the error chain. %v creates a flat string.
chain preserved
Error chain:
ErrNotFound
.Error() = "not found"
errors.Is(outerErr, ErrNotFound) = true // errors.Is traverses chain
// Go code
var ErrNotFound = errors.New("not found")
func dbQuery() error {
err := ErrNotFound
return err
}
🎯
Миссия 1 из 4
Какой глагол fmt.Errorf используется для оборачивания ошибки?