Ошибки в Go
Зачем это на интервью
В Go ошибка — явный результат, поэтому error handling одновременно часть API и наблюдаемости. На интервью E4 должен отличить контекст ошибки от её классификации, обернуть причину через %w, проверить цепочку errors.Is/errors.As и не превратить ожидаемый отказ в panic. На E5 важны стабильный контракт между слоями, retry semantics и безопасное логирование.
Минимум для E4
- Возвращать
errorи проверять её сразу, не игнорировать без документированной причины. - Добавлять операционный контекст через
fmt.Errorf("operation: %w", err). - Проверять sentinel error через
errors.Is, typed error — черезerrors.As. - Отличать ошибку доменного правила от временной инфраструктурной ошибки.
%w создаёт unwrap-цепочку. %v вставляет текст, но теряет программную связь с причиной. Внешний слой добавляет, какая операция не удалась; он не должен переписывать или скрывать полезную первопричину.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func loadUser(id string) error {
if id == "" {
return fmt.Errorf("load user %q: %w", id, ErrNotFound)
}
return nil
}
func main() {
err := loadUser("")
fmt.Println(errors.Is(err, ErrNotFound))
}Sentinel error подходит для малого стабильного набора состояний, о которых вызывающий должен принять решение: ErrNotFound, ErrConflict, context.Canceled. Не сравнивайте wrapped error через ==; после wrapping срабатывает errors.Is.
Углубление для E5/Senior
Граница пакета — граница обещаний. Возвращая чужую sentinel error напрямую или заворачивая её с %w, вы позволяете клиенту зависеть от её identity. Это может быть верно, например для fs.ErrNotExist, но для инфраструктурной реализации часто лучше перевести ошибку в собственную доменную классификацию и сохранить original error в логах/telemetry. Не публикуйте тип ошибки, если не готовы поддерживать поля и поведение как API.
Typed error несёт данные решения: поле, ограничение, retry-after, код поставщика. Используйте errors.As по указателю на target. Реализация Unwrap() error либо Unwrap() []error позволяет error tree; errors.Join (Go 1.20+) полезен, когда несколько cleanup-ошибок одинаково важны. Не применяйте Join механически: вызывающий должен понимать, что означает одновременное наличие нескольких причин.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validateEmail(email string) error {
if email == "" {
return &ValidationError{Field: "email", Msg: "required"}
}
return nil
}
func main() {
err := validateEmail("")
var validation *ValidationError
if errors.As(err, &validation) {
fmt.Println(validation.Field)
}
}Классификация управляет действием: context.Canceled обычно не ретраят и часто не логируют как server error; deadline может быть следствием исчерпанного budget, а не недоступности dependency; conflict отдаётся клиенту без retry; transient network error может быть retryable только для идемпотентной операции и с budget/backoff. Один HTTP status не заменяет внутренний error contract.
Логируйте ошибку с контекстными полями один раз на границе, которая владеет решением о наблюдаемости. Повторное логирование на каждом слое создаёт дубликаты без новой информации. Не включайте в Error() пароль, token, полный request body или PII: текст ошибки часто попадёт в логи, метрики и ответ API.
Ключевые понятия
| Понятие | Смысл | Как проверять |
|---|---|---|
| Sentinel error | Экспортируемый singleton для состояния | errors.Is(err, ErrNotFound) |
| Typed error | Тип с данными, нужными вызывающему | errors.As(err, &target) |
| Wrapping | Добавление контекста с сохранением причины | fmt.Errorf("op: %w", err) |
| Unwrap chain/tree | Причины, доступные Is и As | Unwrap() error / []error |
| Error contract | Стабильные классификации для клиента | Документировать действия, не текст |
Error string предназначен человеку, а не ветвлению программы. Не парсите err.Error() и не проверяйте подстроки. Для transport API делайте явное отображение domain errors в HTTP/gRPC response; оригинальную ошибку не отправляйте клиенту, если она раскрывает внутренности.
Типовые вопросы
- Чем
%wотличается от%vвfmt.Errorf?%wсохраняет error дляerrors.Is/As;%vтолько форматирует текст.
- Когда использовать sentinel error?
- Когда есть небольшое устойчивое состояние, по которому клиент должен ветвиться, без дополнительных данных.
- Когда нужен typed error?
- Когда вызывающему нужны структурированные данные, например поле validation или retry hint.
- Почему нельзя сравнивать wrapped error через
==?- Внешнее значение — новый wrapper; identity исходной ошибки ищет
errors.Isпо цепочке.
- Внешнее значение — новый wrapper; identity исходной ошибки ищет
- Нужно ли оборачивать каждую ошибку?
- Добавляйте контекст, который помогает действию или диагностике; не создавайте шум из повторяющихся «failed» без операции.
- Как обработать две ошибки cleanup?
- Определить приоритет по контракту; при независимой важности использовать
errors.Joinи документировать это.
- Определить приоритет по контракту; при независимой важности использовать
Практика
- Реализуйте repository
FindUser, который преобразует отсутствие строки вErrNotFound.- Критерии готовности: вызывающий использует
errors.Is; ошибка драйвера не сравнивается по строке; operation context содержит ID без секрета.
- Критерии готовности: вызывающий использует
- Создайте
ValidationErrorдля формы регистрации и HTTP mapping.- Критерии готовности: handler использует
errors.As; клиент получает безопасный 400-контракт; оригинальная infrastructure error не раскрывается в ответе.
- Критерии готовности: handler использует
- Добавьте cleanup с двумя независимыми ресурсами.
- Критерии готовности: тест проверяет обе причины через
errors.Is; решение обerrors.Joinобъяснено; success path возвращает literalnil.
- Критерии готовности: тест проверяет обе причины через
Частые ошибки и ловушки
- Терять cause, форматируя её через
%vвместо%w. - Ветвиться по
err.Error()или HTTP-тексту поставщика. - Экспортировать sentinel/typed error случайно и затем считать её внутренней деталью.
- Логировать одну и ту же ошибку на каждом уровне call stack.
- Считать любую ошибку dependency retryable без идемпотентности, deadline и retry budget.
- Возвращать typed-nil pointer как
error.