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

Свои типы ошибок

Два подхода к кастомным ошибкам

Когда простого errors.New недостаточно, в Go есть два подхода:

  1. Sentinel errors — заранее объявленные переменные для сравнения
  2. Кастомные типы — структуры с дополнительными полями

Выбор зависит от того, нужны ли вызывающему коду детали об ошибке.


Sentinel errors: маяки для навигации

Sentinel error — это переменная уровня пакета, которая служит «маяком»:

go
package store
 
import "errors"
 
// Объявляем sentinel errors
var (
    ErrNotFound   = errors.New("запись не найдена")
    ErrDuplicate  = errors.New("запись уже существует")
    ErrForbidden  = errors.New("доступ запрещён")
)
 
func (s *Store) GetUser(id int) (*User, error) {
    user, ok := s.users[id]
    if !ok {
        return nil, fmt.Errorf("GetUser(%d): %w", id, ErrNotFound)
    }
    return user, nil
}

Вызывающий код может реагировать на конкретные ситуации:

go
user, err := store.GetUser(42)
if err != nil {
    if errors.Is(err, store.ErrNotFound) {
        http.Error(w, "пользователь не найден", 404)
        return
    }
    if errors.Is(err, store.ErrForbidden) {
        http.Error(w, "нет доступа", 403)
        return
    }
    http.Error(w, "внутренняя ошибка", 500)
    return
}

Из стандартной библиотеки ты постоянно встречаешь sentinels:

  • io.EOF — конец файла
  • sql.ErrNoRows — запрос вернул 0 строк
  • os.ErrNotExist — файл не существует
  • context.Canceled, context.DeadlineExceeded

Кастомные типы ошибок: когда нужны детали

Sentinel errors хороши когда вызывающему достаточно знать что пошло не так. Кастомный тип нужен когда важно почему и где:

go
// Ошибка с контекстом для HTTP API
type APIError struct {
    StatusCode int
    Code       string  // машиночитаемый код для клиента
    Message    string  // человекочитаемое сообщение
}
 
func (e *APIError) Error() string {
    return fmt.Sprintf("[%d] %s: %s", e.StatusCode, e.Code, e.Message)
}
 
// Использование:
func createUser(u User) error {
    if u.Email == "" {
        return &APIError{
            StatusCode: 400,
            Code:       "INVALID_EMAIL",
            Message:    "email обязателен",
        }
    }
    return nil
}
 
// Обработка в HTTP хендлере:
if err := createUser(u); err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        w.WriteHeader(apiErr.StatusCode)
        json.NewEncoder(w).Encode(map[string]string{
            "code":    apiErr.Code,
            "message": apiErr.Message,
        })
        return
    }
    http.Error(w, "internal error", 500)
}

Паттерн: ошибка с несколькими причинами

Валидация форм часто возвращает несколько ошибок сразу:

go
type ValidationErrors []error
 
func (ve ValidationErrors) Error() string {
    msgs := make([]string, len(ve))
    for i, e := range ve {
        msgs[i] = e.Error()
    }
    return strings.Join(msgs, "; ")
}
 
func (ve ValidationErrors) Is(target error) bool {
    for _, e := range ve {
        if errors.Is(e, target) {
            return true
        }
    }
    return false
}
 
func validateForm(f Form) error {
    var errs ValidationErrors
    if f.Name == "" {
        errs = append(errs, errors.New("имя обязательно"))
    }
    if f.Age < 0 || f.Age > 150 {
        errs = append(errs, fmt.Errorf("некорректный возраст: %d", f.Age))
    }
    if len(errs) > 0 {
        return errs
    }
    return nil
}

Паттерн: опциональный тип ошибки

Иногда нужно добавить метаданные к любой ошибке:

go
type StackError struct {
    err   error
    stack []string  // стек вызовов
}
 
func WithStack(err error) error {
    if err == nil {
        return nil
    }
    return &StackError{
        err:   err,
        stack: captureStack(),
    }
}
 
func (e *StackError) Error() string { return e.err.Error() }
func (e *StackError) Unwrap() error { return e.err }
func (e *StackError) Stack() []string { return e.stack }

Именно так работают популярные библиотеки github.com/pkg/errors и go.uber.org/zap.


Когда НЕ нужно создавать кастомный тип

Джуны часто создают кастомные типы для каждой ситуации. Это излишне:

go
// Излишне сложно:
type FileNotFoundError struct{ Path string }
func (e *FileNotFoundError) Error() string { return "not found: " + e.Path }
 
type PermissionError struct{ Path string }
func (e *PermissionError) Error() string { return "permission denied: " + e.Path }
 
// Достаточно:
var ErrNotFound   = errors.New("not found")
var ErrPermission = errors.New("permission denied")
 
func openFile(path string) error {
    // ...
    return fmt.Errorf("openFile(%q): %w", path, ErrNotFound)
}

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


Правила именования

go
// Sentinel errors: начинать с Err
var ErrNotFound = errors.New("not found")
var ErrTimeout  = errors.New("timeout")
 
// Типы ошибок: заканчивать на Error
type ValidationError struct { ... }
type NetworkError struct { ... }
 
// Не делай так:
var NotFound = errors.New(...)  // неочевидно что это ошибка
type ErrValidation struct { ... }  // смешано именование

Эти конвенции из Effective Go — следуй им, и твой код будет читаться как стандартная библиотека.

Sentinel errors (var ErrNotFound = errors.New(...)) — для проверки через errors.Is. Кастомные типы — когда нужны дополнительные поля (код, контекст). Не создавай типы ошибок без причины.
GO CODE
package store

import "errors"

// sentinel errors — просто значения
var (
    ErrNotFound  = errors.New("not found")
    ErrDuplicate = errors.New("duplicate")
    ErrForbidden = errors.New("forbidden")
)

func GetUser(id int) (*User, error) {
    user, exists := db[id]
    if !exists {
        return nil, ErrNotFound
    }
    if user.Role == "banned" {
        return nil, ErrForbidden
    }
    return user, nil
}

// вызывающий код
user, err := store.GetUser(id)
if errors.Is(err, store.ErrNotFound)  { /* 404 */ }
if errors.Is(err, store.ErrForbidden) { /* 403 */ }
package store — база данных
id=1Aliceadmin
id=2Bobuser
id=3Charliebanned
id=4..N — не существует
sentinel errors
var ErrNotFound = errors.New(...)
var ErrDuplicate = errors.New(...)
var ErrForbidden = errors.New(...)
вызывающий код
GetUser()
🎯
Миссия 1 из 4
Как называются заранее объявленные переменные-ошибки типа var ErrX = errors.New(...)?