Обработка ошибок в Go
Обработка ошибок в Go — обёртывание, errors.Is/As, sentinel-ошибки, defer-очистка, политики повторов.
5 вопросов
JuniorТеорияОчень частоЧто такое тип error в Go и что значит возвращать ошибки как значения?
Что такое тип error в Go и что значит возвращать ошибки как значения?
error — встроенный интерфейс с одним методом Error() string. Любой тип, реализующий его, является ошибкой. Функции возвращают error обычным возвращаемым значением (обычно последним); вызывающий проверяет if err != nil вместо перехвата исключений.
Типичные ошибки
- ✗Думать, что в Go есть исключения для обычных ошибок —
panic/recoverдля неустранимых багов, а не для потока управления - ✗Игнорировать возвращённую ошибку через
_вместо проверкиif err != nil - ✗Считать
errorструктурой, а не интерфейсом — на деле любой тип сError() stringподходит
Уточняющие вопросы
- →Как создать простое значение ошибки через
errors.Newилиfmt.Errorf? - →Что такое sentinel-ошибка и как она сравнивается через
errors.Is?
MiddleКодЧастоКак %w-обёртывание и errors.Is / errors.As работают вместе в Go?
Как %w-обёртывание и errors.Is / errors.As работают вместе в Go?
fmt.Errorf("...: %w", err) оборачивает err, добавляя контекст и сохраняя связь с ним через цепочку Unwrap. errors.Is проходит по цепочке, сопоставляя с sentinel-значением; errors.As проходит по ней, находя ошибку конкретного типа и записывая её в целевой указатель.
Типичные ошибки
- ✗Использовать
%vвместо%w, что теряет связьUnwrap, иerrors.Is/errors.Asбольше не находят совпадение через неё - ✗Сравнивать обёрнутые ошибки через
==вместоerrors.Is— это ломается, как только добавлен контекст - ✗Передавать в
errors.Asне-указатель — это паникует; цель должна быть указателем на тип, реализующий error
Уточняющие вопросы
- →Почему обёртывание через
%wтребует, чтобы под капотом ошибка реализовывала методUnwrap? - →Когда стоит обернуть несколькими
%wв одном вызовеfmt.Errorf?
MiddleДебаггингИногдаПочему этот load всегда возвращает nil-ошибку, даже когда parse падает?
Почему этот load всегда возвращает nil-ошибку, даже когда parse падает?
Функция жёстко возвращает return cfg, nil, поэтому ошибка из parse проглатывается. Родственная классика — cfg, err := parse(data) с := во вложенной области, что затеняет внешний cfg, и распарсенное значение теряется. Исправление — возвращать ошибку при сбое и избегать случайного :=; go vet это ловит.
Типичные ошибки
- ✗Жёстко писать
return cfg, nilвместо возврата настоящей ошибки - ✗Использовать
:=во вложенной области, молча затеняя внешнюю переменную - ✗Считать, что ветка
ifтолько для успеха означает, чтоparseне может упасть
Уточняющие вопросы
- →Как
:=решает, создать новую переменную или переиспользовать внешнюю? - →Что покажет анализ затенения
go vetна этой функции?
SeniorДебаггингИногдаИсправьте ошибки в этом обработчике бронирования HandleBookingOrder.
Исправьте ошибки в этом обработчике бронирования HandleBookingOrder.
Смените сигнатуру на (*Receipt, error) и оборачивайте каждый сбой через %w. Вынесите UnlockUser в defer, который захватывает и оборачивает свою ошибку через именованный возврат. Дайте BookingServiceError метод Error() string, а в цикле через errors.As извлекайте его и проверяйте TryAgain, а не голое поле на interface-ошибке.
Типичные ошибки
- ✗Читать
err.TryAgainнапрямую — в циклеerrхранится как interfaceerror, и поле конкретного типа недоступно безerrors.As - ✗Вызывать
UnlockUserнапрямую вместоdefer, из-за чего раннийreturnпри сбое бронирования теряет блокировку пользователя - ✗Возвращать
nilна пути сбоя блокировки в функции(*Receipt, error), и вызывающий не видит ни чека, ни ошибки
Уточняющие вопросы
- →Почему отложенный unlock должен использовать именованный возвращаемый параметр, чтобы всплыть со своей ошибкой?
- →Как добавить ограниченное число повторов и backoff в цикл
TryAgain?
JuniorКодРедкоВернуть пользовательскую ошибку, не импортируя ни одного пакета
Вернуть пользовательскую ошибку, не импортируя ни одного пакета
error — это встроенный интерфейс с одним методом Error() string. Поэтому вы определяете структуру, даёте ей метод Error() string и возвращаете указатель на неё — импорт не нужен. func (e *customError) Error() string { return "custom error" }, затем return &customError{}. Любой тип, реализующий этот один метод, неявно удовлетворяет error.
Типичные ошибки
- ✗Думать, что
errorживёт в пакете, а не является встроенным интерфейсом - ✗Считать
errorпсевдонимом дляstring, к которому можно привести - ✗Ожидать, что Go автогенерирует
Error()из поля структуры
Уточняющие вопросы
- →Почему здесь для
Error()лучше указательный получатель, а не значимый? - →Как добавление полей в ваш тип ошибки позволит вызывающим разобрать её через
errors.As?