Информативная обработка ошибок в Go

Концепт обработки ошибок в Go довольно интересен в первую очередь тем, что ошибка в Go это просто произвольный объект поддерживающий интерфейс с функцией Error() string. Подобный подход превращает ошибку в некий гибрид Boolean с текстовым описанием и изрядная часть кодовой базы Go проектов выглядит так:

func foo(val int) error {
    if val == 42 {
        return fmt.Errorf("42 is not allowed")
    }
    // normal workflow
    return nil
}
...
err := foo(1)
if err != nil {
// do some error handling
}
// normal workflow


И в принципе тут всё хорошо до тех пор, пока не возникает необходимость в понимании того, что же именно случилось в foo()? Тут на помощь приходит второй не менее распространенный подход с константными ошибками:

var (
    ErrFortyTwo = fmt.Errorf("42 is not allowed")
)
func foo(val int) error {
    if val == 42 {
        return ErrFortyTwo
    }
    // normal workflow
    return nil
}
...
err := foo(1)
if err == ErrFortyTwo {
    // do some error handling for ErrConstant
}
// normal workflow

Но рано или поздно наступает следующая стадия: я хочу иметь более информативную ошибку, куда можно добавить произвольный текст известный только на этапе исполнения. В моем случае нужно было получить даже немного больше:

  • Компонент, в котором произошла ошибка;
  • Код ошибки;
  • Произвольное текстовое описание для ошибки.

Итоговое решение оказалось довольно удобным (по меркам мира Go, разработчики на C++ может и будут плеваться). Для начала стоит защититься от неверных кодов ошибок и за одно облегчить вывод ошибки на экран/в лог.

type Component int
type Code int

//go:generate stringer -type=Component
//go:generate stringer -type=Code
const (
    ErrOne Code = iota
    ErrTwo

    ComponentOne Component = iota
    ComponentTwo
)

Я воспользовался сторонней утилитой stringer, так как имя константы в логе куда как более информативное нежели числовой код, но это опционально. Интерфейс ошибки и его реализация не менее простые:

type CompBaseError interface {
    error
    Code() Code
    Component() Component
}

type ErrorDescription struct {
    component Component
    code Code
    message string
}

func (e *ErrorDescription) Error() string {
    return fmt.Sprintf("%s | %s: %s", e.component, e.code, e.message)
}

func (e *ErrorDescription) Code() Code {
    return e.code
}

func (e *ErrorDescription) Component() Component {
    return e.component
}

Работа с CompBaseError выходит несколько более громоздкой за счет необходимости преобразования общего интерфейса error к конкретному интерфейсу CompBaseError, но так как булевый вариант использования никуда не девается, то подобный подход выглядит достаточно разумной платой за возможность получить расширенную информацию.

func foo(val int) error {
    if val == 42 {
        return ErrorDescription{ComponentOne, ErrTwo, "42 is not allowed"}
    }
    // normal workflow
    return nil
}
...
err := foo(1)
if errCmpBase, ok := err.(CompBaseError); ok && errCmpBase.Component() == ComponentOne && errCmpBase.Code() == ErrTwo {
    // do some error handling for error from ComponentOne with code ErrTwo
}
// normal workflow

Благодаря утилите stringer упомянутой выше, ошибка при выводе будет выглядеть следующим образом:

ComponentOne | ErrTwo: 42 is not allowed

Не сказать что я счастлив, но свои плюсы в подходе Go к обработке ошибок безусловно есть, особенно в сочетании с линтерами не позволяющими игнорировать ошибки.

Leave a Reply