Код-ревью 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-ошибкой и обёрткой:

Что должен найти ревьюер

  1. Ошибкастрока 16
    Возвращается типизированный nil-указатель *NotFoundError как error: интерфейс не равен nil, и даже успешный поиск выглядит как ошибка. В успешной ветке нужно вернуть литерал nil.
  2. Ошибкастрока 10
    Сравнение через == не сработает, если драйвер или слой ниже обернул ошибку. Нужно errors.Is(dbErr, sql.ErrNoRows).
  3. Стильстрока 13
    Ошибка и логируется, и возвращается: выше её залогируют ещё раз, в логах будут дубли. Либо обработать, либо вернуть с контекстом.
  4. Ошибкастрока 14
    %v превращает ошибку в текст, и вызывающий не сможет проверить причину через errors.Is/As. Нужно %w.
  5. Ошибкастрока 22
    FindUser возвращает тип NotFoundError, а не sentinel ErrNotFound, поэтому проверка никогда не сработает. Нужно errors.As(err, &nf) или единый подход: возвращать ErrNotFound, обёрнутый с id.
  6. Ошибкастрока 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 ошибок.

Термины по теме