Пример обработки связанных ошибок в Go с помощью объекта CustomError

Мой фрагмент кода представляет собой способ обработки связанных ошибок. Мне нужен код-ревью, чтобы понять, является ли мой путь идиоматическим и актуальным для 2021 года?

Мои личные требования:

  1. Я хочу, чтобы каждый пакет моего проекта имел errors.go файл с определенными ошибками дозорного типа. Это, на мой взгляд, значительно улучшает удобочитаемость / ремонтопригодность.
  2. Я хочу использовать новые errors.Is/As функциональность.
  3. Я хочу скрыть детали реализации базовых пакетов. В моем фрагменте кода — я не хочу web пакет, чтобы знать что-нибудь о repository.NotFoundError а также repository.DatabaseError. Но я хочу, чтобы моя веб-ошибка верхнего уровня имела полную цепочку базовых строк ошибок (возможно, контекстов ошибок) для их описания в журналах, HTTP-ответах и ​​т. Д.

Вот мой скромный фрагмент (его можно запустить с помощью теста go):


package main

import (
    "errors"
    "fmt"
    "testing"
)

// the code below should be kept in some kind of common place 

type CustomError struct {
    Message string
    Child   error
}

func (cerr *CustomError) Error() string {
    if cerr.Child != nil {
        return fmt.Sprintf("%s: %s", cerr.Message, cerr.Child.Error())
    }
    return cerr.Message
}

func (cerr *CustomError) Unwrap() error {
    return cerr.Child
}

func (cerr *CustomError) Wrap(child error) error {
    cerr.Child = child
    return cerr
}

func CustomErrorBuilder(message string, child error) *CustomError {
    return &CustomError{Message: message, Child: child}
}


// the code below represents the 'repository' package


var (
    NotFoundError = CustomErrorBuilder("NotFoundError", nil)
    DatabaseError = CustomErrorBuilder("DatabaseError", nil)
)

func QueryUser(id int) (string, error) {
    if id == 0 {
        return "User 0", nil
    }
    if id == 1 {
        return "User 1", nil
    }
    if id == 100 {
        return "", DatabaseError
    }

    return "", NotFoundError
}

// the code below represents the 'core' package

var (
    InfrastructureError = CustomErrorBuilder("InfrastructureError", nil)
    BusinessLogicError  = CustomErrorBuilder("BusinessLogicError", nil)
)

func UserHasName(id int, name string) (bool, error) {
    userName, err := QueryUser(id)
    if err != nil {
        if errors.Is(err, NotFoundError) {
            return false, BusinessLogicError.Wrap(NotFoundError)
        }
        if errors.Is(err, DatabaseError) {
            return false, InfrastructureError.Wrap(DatabaseError)
        }
    }

    if userName == name {
        return true, nil
    } else {
        return false, nil
    }
}

// the code below represents the 'web' package

func Handler(id int, name string) (int, string) {
    result, err := UserHasName(id, name)
    if err != nil {
        if errors.Is(err, BusinessLogicError) {
            return 404, fmt.Sprintf("NOT FOUND %v", err)
        }
        if errors.Is(err, InfrastructureError) {
            return 500, fmt.Sprintf("INTERNAL SERVER ERROR %v", err)
        }
    }
    return 200, fmt.Sprintf("OK %t", result)
}

// This test checks errors wrapping

func TestHandler(t *testing.T) {
    testCases := []struct {
        userId         int
        userName       string
        expectedStatus int
        expectedBody   string
    }{
        {userId: 0, userName: "User 0", expectedStatus: 200, expectedBody: "OK true"},
        {userId: 1, userName: "User 0", expectedStatus: 200, expectedBody: "OK false"},
        {userId: 2, userName: "", expectedStatus: 404, expectedBody: "NOT FOUND BusinessLogicError: NotFoundError"},
        {userId: 100, userName: "", expectedStatus: 500, expectedBody: "INTERNAL SERVER ERROR InfrastructureError: DatabaseError"},
    }

    for i, tcase := range testCases {
        t.Run(fmt.Sprintf("Case %v", i), func(t *testing.T) {
            status, body := Handler(tcase.userId, tcase.userName)
            if status != tcase.expectedStatus {
                t.Fatalf("%v != %v", status, tcase.expectedStatus)
            }
            if body != tcase.expectedBody {
                t.Fatalf("%s != %s", body, tcase.expectedBody)
            }
        })
    }
}
```

1 ответ
1

Пара вещей:


Если вы не возвращаете какие-либо подробные данные в ваших ошибках (помимо дополнительной строки ошибки), тогда настраиваемая ошибка может быть излишней. Того же можно добиться с помощью:

if errors.Is(err, NotFound) {
        return fmt.Errorf("Business Logic Error: %w", err)
}

fmt.Errorf с %w wrap-verb оборачивает новую ошибку добавленным сообщением об ошибке — сохраняя исходную ошибку, если нужно развернуть или использовать errors.Is для соответствия.


В UserHasName есть ошибка провала:

if err != nil {
    if errors.Is(err, NotFoundError) { return /* */ }
    if errors.Is(err, DatabaseError) { return /* */ }
    
    // if err matches neither of the above checks, then the error is lost
}

Применение вышеуказанного рефакторинга / исправления ошибок: https://play.golang.org/p/N5_PAiJKzRh

  • Спасибо за пример. Я согласен, что fmt.Errorf("Business Logic Error: %w", err) может частично решить мою проблему, но, насколько я понимаю, в этом случае я буду вынужден оставить "Business Logic Error:" строка ошибки в двух местах: BusinessLogicError = CustomErrorBuilder("BusinessLogicError", nil) а также fmt.Errorf("Business Logic Error: %w", err) Я хотел избежать этого дублирования и использовать только дозорные ошибки.

    — Серж Пискунов



  • В предоставленной мной ссылке на игровую площадку строка ошибки определяется только один раз (строка 14) и используется повторно (строка 35).

    — colm.anseo



  • О, я вижу. В этом есть смысл. Спасибо!

    — Серж Пискунов

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *