Принуждение к 1:1 соответствию между Ошибками, определенными в библиотеке, и Счетчиками, определенными в потребителе.

Вопрос или проблема

У меня есть библиотека, которая определяет некоторые категории ошибок для определенных функций. Например:

var (
    ErrNotFound         = errors.New("не найдено")
    ErrFailedParsing    = errors.New("ошибка парсинга")
    ErrFailedProcessing = errors.New("ошибка обработки")
)

func Run() error {
    if 1 != 2 {
        return ErrNotFound
    }

    return nil
}

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

Например:

var (
    NotFoundCounter         int
    FailedParsingCounter    int
    FailedProcessingCounter int
)

func main() {
    err := Run()
    if err != nil {
        switch err {
        case ErrNotFound:
            NotFoundCounter += 1
        case ErrFailedParsing:
            FailedParsingCounter += 1
        case ErrFailedProcessing:
            FailedProcessingCounter += 1
        }
    }

    fmt.Printf("счетчики: %d %d %d\n", NotFoundCounter, FailedParsingCounter, FailedProcessingCounter)
}

Это работает, но неустойчиво.

Если библиотека будет изменена для добавления новой категории ошибок ErrServiceUnavailable, она будет проигнорирована счётчиком (или, в лучшем случае, будет отнесена к некатегоризированной ошибке во время выполнения).

Я хотел бы механизм, который позволяет мне установить соответствие 1:1 между ошибками библиотеки и счетчиками во время сборки (например, в Rust я мог бы обеспечить исчерпывающий switch/case).

Я вполне уверен, что могу найти решение, используя одно из:

  1. Рефлексию (например, в юнит-тестах)
  2. Изменение библиотеки для регистрации метрик
  3. go vet

Но я бы назвал все эти способы обходными.

Есть ли лучший способ? Я полагаю, что то, что я пытаюсь сделать, довольно распространено.

Ответ или решение

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

Подход с использованием интерфейсов и констант

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

Шаг 1: Определение интерфейса

Создайте интерфейс для ошибок:

type ErrorType interface {
    Error() string
    CounterName() string
}

Шаг 2: Реализация ошибок

Теперь реализуйте этот интерфейс для каждой ошибки:

type NotFoundError struct{}

func (e NotFoundError) Error() string {
    return "not found"
}

func (e NotFoundError) CounterName() string {
    return "NotFoundCounter"
}

// Аналогично для остальных ошибок

Шаг 3: Обновление библиотеки

Измените вашу библиотеку, чтобы возвращать ошибки, реализующие этот интерфейс:

var (
    ErrNotFound         ErrorType = NotFoundError{}
    ErrFailedParsing    ErrorType = FailedParsingError{}
    ErrFailedProcessing ErrorType = FailedProcessingError{}
)

Шаг 4: Обработка ошибок в клиенте

Теперь при обработке ошибок в клиентском коде вы можете использовать рефлексию или сопоставление по типу для увеличения нужного счётчика:

func main() {
    err := Run()
    if err != nil {
        switch e := err.(type) {
        case NotFoundError:
            NotFoundCounter += 1
        case FailedParsingError:
            FailedParsingCounter += 1
        case FailedProcessingError:
            FailedProcessingCounter += 1
        }
    }

    fmt.Printf("counters: %d %d %d\n", NotFoundCounter, FailedParsingCounter, FailedProcessingCounter)
}

Шаг 5: Компиляция ошибок

Чтобы убедиться, что вы не забыли обработать ошибку, можно воспользоваться особенностями компилятора. Если вы добавите новую ошибку и не обновите обработчик счётчиков, компилятор выдаст ошибку о неучтённом типе.

Заключение

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

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

Капча загружается...