В большинстве языков для ошибок есть исключения: что-то пошло не так — бросаем throw, ловим catch. В Go исключений нет. Ошибка — это обычное значение, которое функция возвращает наравне с результатом. Поначалу это выглядит многословно, но у такого подхода есть важное преимущество: путь каждой ошибки виден прямо в коде.
Ошибка как возвращаемое значение
В Go принято возвращать error последним значением. Если всё прошло хорошо — возвращаем nil. Если нет — возвращаем ошибку и нулевое значение для результата.
func findProduct(id int) (Product, error) {
if id <= 0 {
return Product{}, errors.New("id должен быть положительным")
}
// ...
return product, nil
}
Вызывающий обязан явно проверить результат:
product, err := findProduct(42)
if err != nil {
// обрабатываем ошибку
return
}
// используем product
Проигнорировать ошибку по невнимательности сложнее: если не проверить, компилятор не выдаст ошибку, но статический анализатор (go vet, staticcheck) об этом предупредит. Намеренно проигнорировать можно через _ = ... — это явный сигнал, что вы осознанно пропускаете ошибку.
Оборачивание ошибок: %w и контекст
Когда ошибка поднимается по стеку вызовов, каждый уровень добавляет контекст — где именно что-то пошло не так. Для этого используют fmt.Errorf с глаголом %w:
func (r *ProductRepository) Find(ctx context.Context, id int) (Product, error) {
row := r.db.QueryRow(ctx, `SELECT id, name FROM products WHERE id = $1`, id)
var p Product
if err := row.Scan(&p.ID, &p.Name); err != nil {
return Product{}, fmt.Errorf("find product %d: %w", id, err)
}
return p, nil
}
Благодаря %w исходная ошибка «спрятана» внутри новой, но не потеряна. Если распечатать такую цепочку — получим читаемый путь: "find product 42: no rows in result set".
errors.Is и errors.As: проверка сквозь обёртки
Раз ошибки оборачиваются, простое сравнение err == someErr перестаёт работать — обёртка уже другой объект. Для этого есть две функции:
errors.Is — проверяет, есть ли в цепочке обёрток конкретная ошибка:
if errors.Is(err, pgx.ErrNoRows) {
// база вернула «нет строк»
}
Это работает даже если err — это fmt.Errorf("...: %w", pgx.ErrNoRows).
errors.As — пытается найти в цепочке ошибку нужного типа и записывает её в переменную. Полезно, когда ошибка несёт данные:
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("поле:", valErr.Field)
}
Sentinel-ошибки: различаем виды отказов
Часто нужно, чтобы вызывающий код мог различить причину ошибки: не найдено, нет доступа, неверный ввод. Для этого объявляют sentinel-ошибки — именованные значения на уровне пакета:
var ErrProductNotFound = errors.New("product not found")
var ErrForbidden = errors.New("forbidden")
В коде репозитория оборачиваем техническую ошибку в доменную:
if errors.Is(err, pgx.ErrNoRows) {
return Product{}, ErrProductNotFound
}
Теперь вышестоящий код не знает про pgx.ErrNoRows и не зависит от базы данных — он работает только с ErrProductNotFound. Если ошибке нужны данные (например, какой именно id не найден), её делают типом:
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d not found", e.Resource, e.ID)
}
И достают через errors.As.
Перевод доменных ошибок в HTTP-коды
Задача обработчика запроса — перевести доменную ошибку в правильный HTTP-статус. Лучше всего собрать этот перевод в одной функции:
func errorStatus(err error) int {
switch {
case errors.Is(err, ErrProductNotFound):
return http.StatusNotFound
case errors.Is(err, ErrForbidden):
return http.StatusForbidden
case errors.Is(err, ErrBadInput):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
Обработчик вызывает её при любой ошибке:
product, err := h.useCase.Execute(r.Context(), cmd)
if err != nil {
http.Error(w, err.Error(), errorStatus(err))
return
}
Такой подход хорош тем, что весь перевод «доменное → HTTP» сосредоточен в одном месте. Добавить новый код или изменить маппинг — правка в одной функции.
Panic: когда это уместно
panic в Go — это не аналог исключений. Он предназначен для ситуаций, которых в нормальной работе быть не должно: обращение к nil-указателю, выход за границы среза, программная ошибка, которую невозможно обработать.
Бизнес-ошибки («запись не найдена», «нет прав», «неверный формат») — это всегда error, а не panic.
Чтобы случайная паника в одном запросе не положила весь сервер, в HTTP-промежуточном слое ставят recover:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Использовать panic/recover для управления потоком программы как замену исключениям — плохая практика в Go.
Коротко
- В Go ошибка — это возвращаемое значение, путь каждой ошибки виден в коде.
fmt.Errorf("...: %w", err)добавляет контекст, сохраняя исходную ошибку.errors.Isпроверяет конкретное значение сквозь цепочку обёрток.errors.Asизвлекает ошибку нужного типа, если она несёт данные.- Sentinel-ошибки (
var ErrNotFound = errors.New(...)) позволяют различать виды отказов без привязки к технике. - Перевод доменных ошибок в HTTP-коды собирают в одной функции-маппере.
panic— для невосстановимых ситуаций, не для бизнес-логики;recoverв промежуточном слое защищает сервер от падения.
Что почитать дальше
- Обработчики и JSON в Go — как строить HTTP-обработчики и отдавать JSON-ответы.
- Промежуточный слой в Go — как подключать recover и другие промежуточные обработчики.
- Context и отмена — как передавать дедлайны и отменять операции через context.Context.