Мой фрагмент кода представляет собой способ обработки связанных ошибок. Мне нужен код-ревью, чтобы понять, является ли мой путь идиоматическим и актуальным для 2021 года?
Мои личные требования:
- Я хочу, чтобы каждый пакет моего проекта имел
errors.go
файл с определенными ошибками дозорного типа. Это, на мой взгляд, значительно улучшает удобочитаемость / ремонтопригодность. - Я хочу использовать новые
errors.Is/As
функциональность. - Я хочу скрыть детали реализации базовых пакетов. В моем фрагменте кода — я не хочу
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 ответ
Пара вещей:
Если вы не возвращаете какие-либо подробные данные в ваших ошибках (помимо дополнительной строки ошибки), тогда настраиваемая ошибка может быть излишней. Того же можно добиться с помощью:
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
О, я вижу. В этом есть смысл. Спасибо!
— Серж Пискунов