🚧 Errors in Go: Хватит сравнивать ошибки через == Знакомая картина?
Знакомая картина? Вы пишете запрос к базе, получаете ошибку, оборачиваете её для логов и возвращаете наверх:
// Слой репозитория
func GetUser() error {
err := db.QueryRow(...)
if err != nil {
// Обернули, чтобы дать контекст
return fmt.Errorf("getUser failed: %v", err)
}
}
А на слое выше (в хендлере) кто-то пытается проверить, не была ли это ошибка "пользователь не найден":
// Слой хендлера
err := GetUser()
// 🤡 Ошибка! Это условие НИКОГДА не выполнится
if err == sql.ErrNoRows {
return 404
}
Почему не выполнится? Потому что fmt.Errorf с глаголом %v создал новую строку. Оригинальная ошибка sql.ErrNoRows стерлась. Чтобы это починить, джуны начинают парсить текст ошибки через strings.Contains() - и это прямой путь в ад.
С версии Go 1.13 язык предлагает элегантное решение - распаковку "матрешки" ошибок.
1. Упаковываем: глагол %w
Чтобы добавить контекст, но сохранить оригинальную ошибку внутри, используйте %w (wrap):
return fmt.Errorf("getUser failed: %w", err)Теперь ваша ошибка это матрешка. Снаружи текст "getUser failed", а внутри лежит sql.ErrNoRows.
2. Распаковываем значения: errors.Is
Забудьте про оператор == при работе с ошибками, если есть хотя бы малейший шанс, что ошибка обернута. Используйте errors.Is.
Эта функция рекурсивно заглядывает внутрь каждой матрешки и ищет совпадение:
if errors.Is(err, sql.ErrNoRows) {
// Выполнится успешно, даже если ошибка была обернута 10 раз!
return 404
}
3. Распаковываем типы: errors.As
errors.Is нужен для проверки конкретных переменных-синглтонов (sentinel errors). Но что если вы используете кастомные структуры ошибок с дополнительными полями (например, HTTP статус-код)?
Встречайте errors.As. Это безопасный способ достать конкретный тип из цепочки ошибок:
type MyDomainError struct {
Msg string
Code int
}
func (e *MyDomainError) Error() string { return e.Msg }
// ... где-то в коде проверяем:
var domainErr *MyDomainError
// Если в цепочке err есть тип *MyDomainError,
// errors.As запишет его в переменную domainErr и вернет true
if errors.As(err, &domainErr) {
fmt.Println("HTTP Status:", domainErr.Code)
}
🔥 Senior Tip: Не оборачивайте всё подряд
Если вы оборачиваете ошибку через %w, она становится частью вашего публичного API.
Если слой базы данных возвращает обернутую pgx.ErrNoRows, а слой HTTP-хендлера проверяет её через errors.Is, вы создали жесткую связность (coupling). Хендлер теперь знает про базу данных!
• Используйте %w, когда хотите позволить вызывающему коду реагировать на причину.
• Используйте %v (или создавайте новую чистую ошибку), когда хотите скрыть детали реализации нижнего уровня.
#golang #errors #cleancode #bestpractices
👉 @golang_lib