Оборачивание ошибок
Зачем оборачивать ошибки?
Представь: пользователь нажал «Сохранить», и что-то пошло не так. Ты получаешь ошибку:
permission denied
Что это? Какой файл? На каком этапе? Непонятно.
А вот хорошая ошибка:
saveProfile: writeFile: open /home/user/.config/app.json: permission denied
Каждый уровень добавил контекст. Это и есть оборачивание ошибок — каждая функция добавляет свой слой информации, сохраняя оригинальную ошибку внутри.
fmt.Errorf с %w
До Go 1.13 оборачивание требовало кастомных типов. Теперь достаточно %w:
// Низкий уровень: файловая операция
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: найти ошибку в цепочке
Раньше джуны писали так:
// ПЛОХО: сравнение строк или прямое ==
if err.Error() == "file not found" { ... } // хрупко
if err == os.ErrNotExist { ... } // сломается если err обёрнутаerrors.Is раскручивает цепочку оборачиваний и ищет нужную ошибку:
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Практический пример — разное поведение в зависимости от причины ошибки:
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 — «есть ли в цепочке ошибка такого типа, и дай мне её»:
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: частая ошибка
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():
wrapped := fmt.Errorf("контекст: %w", originalErr)
// Получить оригинальную ошибку:
inner := errors.Unwrap(wrapped)
fmt.Println(inner == originalErr) // trueЕсли ты создаёшь кастомный тип ошибки и хочешь, чтобы он участвовал в цепочке — реализуй Unwrap():
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 будут раскручивать через это
}Рекомендации по оборачиванию
Добавляй контекст на каждом уровне:
// Хорошо: контекст добавляет информацию
return fmt.Errorf("getUserByEmail(%q): %w", email, err)
// Плохо: контекст ничего не говорит
return fmt.Errorf("error: %w", err)
return fmt.Errorf("failed: %w", err)Не оборачивай дважды одно и то же:
// Плохо: "database" повторяется на каждом уровне
// database: database: database: connection refusedОборачивай ошибки только там, где добавляешь новую информацию. Если функция просто пробрасывает ошибку без контекста — возможно, она вообще не нужна.