Ошибки: wrapping, sentinel, panic

О лекции

Ошибка в Go — значение, которое возвращают и оборачивают. Sentinel проверяют через errors.Is, типы — через As. panic оставляют багам, не веткам вроде невалидного ввода.

Рантайм и инструменты Go golang собеседование

Идиома Go — (T, error) и явная проверка. Это шумно, зато видно, где сбой может произойти. Ошибка — значение, не исключение как основной поток управления.

На бэкенде ошибка пересекает слои: драйвер БД, репозиторий, use case, HTTP. Без контекста на каждом слое в логе остаётся голое no rows без user id.

Аналогия: вместо взрыва в середине функции вы возвращаете записку «не вышло, вот почему». Каждый этаж дописывает к записке, где он был. На входе в здание (HTTP) читают записку и решают: 404, 500 или повтор.

Wrap, Is, As

Обёртка fmt.Errorf("...: %w", err) сохраняет цепочку для errors.Is и errors.As. Процент %v цепочку рвёт: снаружи строка есть, sentinel уже не найти. Свой тип ошибки (с кодом для HTTP, с полем path) достают через As, не через сравнение строк Error().

if err != nil {
	return fmt.Errorf("save user %s: %w", id, err)
}

if errors.Is(err, sql.ErrNoRows) {
	return ErrNotFound
}

var pe *os.PathError
if errors.As(err, &pe) {
	return fmt.Errorf("path %s: %w", pe.Path, pe)
}

Wrap через %w. Sentinel — Is, структурированный тип — As.

Пример. Репозиторий обернул sql.ErrNoRows через %w. Хендлер делает errors.Is(err, sql.ErrNoRows) и отдаёт 404. Если обернули через %v, Is уже не найдёт sentinel — останется 500.

Sentinel вроде io.EOF после wrap нельзя сравнивать через ==: указатель другой, цепочка есть. Is идёт по цепочке. Свои пакетные var ErrNotFound = errors.New(...) — тоже sentinel: их тоже Is, и их не стоит создавать заново каждый раз строкой. Не логируйте и не возвращайте одну и ту же ошибку на каждом слое без решения: либо wrap и проброс, либо лог на границе (HTTP) и ответ клиенту.

panic и recover

Паника раскручивает стек текущей горутины. Это для инвариантов «сюда не должны были попасть»: сломанный индекс, невозможное состояние. Валидация ввода, «пользователь не найден», таймаут соседа — это error. HTTP-сервер ловит панику хендлера, чтобы не убить процесс. Это страховка, не контракт API.

recover имеет смысл только в defer той же горутины. Паника в дочерней G процесс уронит, если там нет своего recover. Глотать панику и продолжать с полусломанным состоянием опасно. Глотать err молча (_ = f() без причины) — тот же класс слепоты, только тише.

Пример. В хендлере на пустой email сделали panic("bad email"). Это HTTP 400, не сломанный инвариант. Нужен return errValidation. recover на границе сервера спасёт процесс, но API станет случайным 500.

Типичная ошибка

Сравнивать обёрнутый sentinel через == или strings.Contains(err.Error()). Паниковать на валидации запроса. Забыть %w и потерять Is. Залогировать на каждом слое и ещё вернуть err — дубли и шум. Либо наоборот проглотить err без лога.

На собеседовании

Возвращаем error, оборачиваем %w, проверяем Is и As. panic — для сломанного состояния программы, не для HTTP 400. Не глотайте err. На границе — лог и код ответа, внутри — wrap с контекстом операции.