Код-ревью Middle Ошибки, defer, panic и recover спрашивают часто обновлено
Код-ревью Go: sentinel-ошибки, typed nil и panic
Вопрос
Поиск пользователя и обработчик. Найдите ошибки в работе с ошибками.
var ErrNotFound = errors.New("not found")
type NotFoundError struct{ ID int }
func (e NotFoundError) Error() string { return fmt.Sprintf("user %d not found", e.ID) }
func FindUser(id int) (*User, error) {
var err *NotFoundError
u, dbErr := db.Get(id)
if dbErr == sql.ErrNoRows {
err = &NotFoundError{ID: id}
} else if dbErr != nil {
log.Printf("db error: %v", dbErr)
return nil, fmt.Errorf("find user: %v", dbErr)
}
return u, err
}
func Handler(id int) string {
u, err := FindUser(id)
if err != nil {
if err == ErrNotFound {
return "404"
}
panic(err)
}
return u.Name
}
Коротко
Исправленный вариант с одной sentinel-ошибкой и обёрткой:
Что должен найти ревьюер
- Ошибкастрока 16Возвращается типизированный nil-указатель
*NotFoundErrorкакerror: интерфейс не равен nil, и даже успешный поиск выглядит как ошибка. В успешной ветке нужно вернуть литерал nil. - Ошибкастрока 10Сравнение через
==не сработает, если драйвер или слой ниже обернул ошибку. Нужноerrors.Is(dbErr, sql.ErrNoRows). - Стильстрока 13Ошибка и логируется, и возвращается: выше её залогируют ещё раз, в логах будут дубли. Либо обработать, либо вернуть с контекстом.
- Ошибкастрока 14
%vпревращает ошибку в текст, и вызывающий не сможет проверить причину через errors.Is/As. Нужно%w. - Ошибкастрока 22FindUser возвращает тип NotFoundError, а не sentinel ErrNotFound, поэтому проверка никогда не сработает. Нужно
errors.As(err, &nf)или единый подход: возвращать ErrNotFound, обёрнутый с id. - Ошибкастрока 25Паника на обычной ошибке базы роняет обработку запроса. Ошибку нужно вернуть или превратить в ответ 500.
Разбор
Исправленный вариант с одной sentinel-ошибкой и обёрткой:
var ErrNotFound = errors.New("not found")
func FindUser(ctx context.Context, id int) (*User, error) {
u, err := db.Get(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
return u, nil
}
func Handler(ctx context.Context, id int) (string, int) {
u, err := FindUser(ctx, id)
switch {
case errors.Is(err, ErrNotFound):
return "not found", http.StatusNotFound
case err != nil:
slog.ErrorContext(ctx, "find user", "err", err)
return "internal error", http.StatusInternalServerError
}
return u.Name, http.StatusOK
}
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.