Как оборачивать ошибки в Go и не потерять машинную проверку причины

В лог добавили контекст через fmt.Errorf, но проверка ошибки базы перестала срабатывать. Если вместо цепочки ошибок получилась только строка, обработчик уже не может надёжно отличить отсутствие записи от временного отказа.

Как разобрать ситуацию

Используйте %w, когда намерены сохранить причину для errors.Is или errors.As. Текст сообщения предназначен человеку, а ветвление поведения должно опираться на тип или известное значение ошибки. При этом публичный контракт не обязан раскрывать все внутренние причины: на границе API их переводят в стабильный безопасный код.

Пример и ожидаемый результат

В примере ошибка not found оборачивается контекстом заказа. errors.Is продолжает её узнавать, хотя полная строка уже изменилась. Вторая ошибка создана через %v и сохраняет только текст, поэтому машинная проверка возвращает false.

package main

import (
	"errors"
	"fmt"
)

func main() {
	base := errors.New("not found")
	a := fmt.Errorf("order 17: %w", base)
	b := fmt.Errorf("order 17: %v", base)
	fmt.Println(errors.Is(a, base), errors.Is(b, base))
}

Ожидаемый вывод — true false. Сохранённая цепочка позволяет добавить подробности для диагностики без уничтожения программного сигнала.

Проверка перед применением

  • Проверьте распознавание причины после двух уровней обёртки.
  • Не сравнивайте err.Error() с полным текстом: добавление полезного контекста сломает такую логику.
  • Разделите ошибки для логирования и информацию, которую допустимо вернуть внешнему клиенту.

Тесты должны проверять наблюдаемое решение: например, отсутствующий заказ даёт 404, а потерянное соединение не маскируется под отсутствие. Одного теста на наличие слова not found в логе недостаточно. Такой подход сохраняет полезную диагностику и делает изменение текста безопасным для поведения сервиса.

Границы решения

Не каждая внутренняя ошибка должна становиться частью внешнего API пакета. Если вызывающему коду нельзя зависеть от реализации хранилища, используйте собственный согласованный тип или sentinel на границе слоя.

Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс

Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.

Техническая справка: pkg.go.dev: документация, go.dev: документация.

Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: Go Decoder.UseNumber: как не округлить большой ID в map[string]any, Плюс и амперсанд потерялись в URL Go-клиента: как собрать query.