Свои типы ошибок
Два подхода к кастомным ошибкам
Когда простого errors.New недостаточно, в Go есть два подхода:
- Sentinel errors — заранее объявленные переменные для сравнения
- Кастомные типы — структуры с дополнительными полями
Выбор зависит от того, нужны ли вызывающему коду детали об ошибке.
Sentinel errors: маяки для навигации
Sentinel error — это переменная уровня пакета, которая служит «маяком»:
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
}Вызывающий код может реагировать на конкретные ситуации:
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 хороши когда вызывающему достаточно знать что пошло не так. Кастомный тип нужен когда важно почему и где:
// Ошибка с контекстом для 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)
}Паттерн: ошибка с несколькими причинами
Валидация форм часто возвращает несколько ошибок сразу:
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
}Паттерн: опциональный тип ошибки
Иногда нужно добавить метаданные к любой ошибке:
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.
Когда НЕ нужно создавать кастомный тип
Джуны часто создают кастомные типы для каждой ситуации. Это излишне:
// Излишне сложно:
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)
}Создавай кастомный тип только если вызывающему коду нужны поля ошибки, а не просто её идентификация.
Правила именования
// 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 — следуй им, и твой код будет читаться как стандартная библиотека.