ИЗЯЩНАЯ РАБОТА С ОШИБКАМИ В GO, ИЛИ КАК ИХ СРАВНИВАТЬОбычное
err == myErr в Go работает не всегда так, как ты ожидаешь. Особенно если ты оборачиваешь ошибку через
fmt.Errorf("context: %w", err). В таких случаях Go предоставляет более мощные инструменты —
errors.Is и
errors.As.
❓Как работает
errors.IsКогда ты используешь
%w в
fmt.Errorf (врапинг ошибки), ты как бы упаковываешь одну ошибку внутрь другой, добавляя контекст.
errors.Is умеет разворачивать такие обёртки и проверять, есть ли внутри нужная ошибка:
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("ошибка в поиске: %w", ErrNotFound)
if errors.Is(err, ErrNotFound) {
fmt.Println("Ничего не найдено")
}Под капотом
errors.Is делает так: если ошибка обёрнута через
%w, она вызывает
Unwrap() и идёт по цепочке, пока не найдёт нужную. Если ты просто сравнишь
err == ErrNotFound, то получишь
false, потому что это уже не тот же объект — он обёрнут.
❓А что делает
errors.AsИногда нужно не просто узнать, была ли ошибка определённого типа, но и получить доступ к её полям. Например:
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("code: %d, msg: %s", e.Code, e.Msg)
}
err := &MyError{Code: 499, Msg: "custom"}
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("У нас ошибка кода", myErr.Code)
}Ты можешь доставать полезные данные из ошибки, если она реализует нужный тип. Это особенно удобно, когда у тебя есть логика, завязанная на тип ошибки (например, логирование по коду, трассировка и т.д.).
❓Как кастомизировать сравнение — метод
Is()Ты даже можешь сам определить, когда твоя ошибка считается «равной» другой. Для этого нужно реализовать метод
Is(target error) в своей реализации:
type HttpError struct {
Code int
Msg string
}
func (e *HttpError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Msg)
}
func (e *HttpError) Is(target error) bool {
t, ok := target.(*HttpError)
return ok && e.Code == t.Code
}Теперь, даже если сообщения об ошибках разные, но коды совпадают —
errors.Is вернёт
true:
var Err404 = &HttpError{Code: 404}
err := fmt.Errorf("обёрнутая ошибка: %w", &HttpError{Code: 404, Msg: "page not found"})
if errors.Is(err, Err404) {
fmt.Println("Обнаружена 404")
}❓Когда и где это реально нужно (примеры из практики)
— В библиотеках: ты возвращаешь ошибки вроде
ErrInvalidConfig или
ErrNotConnected, и хочешь, чтобы пользователи могли их отловить независимо от контекста
— В сервисах: API может возвращать ошибки с кодами, а внутри тебе удобно описывать их кастомными типами — и потом ловить их по
errors.Is— В middleware: например, хочешь логировать всё, кроме
ErrUnauthorized — тогда
errors.Is(err, ErrUnauthorized) спасёт от лишнего шума
— В gRPC/HTTP-хендлерах: удобно классифицировать ошибки и автоматически подставлять нужный статус код в ответе
— В CLI-инструментах: логика выхода с кодами ошибок может зависеть от типа ошибки, а не только от текста
Если ты до сих пор сравниваешь ошибки через
==, самое время освоить
errors.Is и
errors.As. Это делает твой код гибким, читаемым и гораздо ближе к надёжной архитектуре. Go даёт тебе все инструменты — просто используй их.
Больше примеров работы с ошибками даю в
бесплатном уроке на своем
курсе Go! P.S. все еще действует промокод на скидку 30% - FEIN