Вопрос или проблема
У меня есть библиотека, которая определяет некоторые категории ошибок для определенных функций. Например:
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).
Я вполне уверен, что могу найти решение, используя одно из:
- Рефлексию (например, в юнит-тестах)
- Изменение библиотеки для регистрации метрик
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: Компиляция ошибок
Чтобы убедиться, что вы не забыли обработать ошибку, можно воспользоваться особенностями компилятора. Если вы добавите новую ошибку и не обновите обработчик счётчиков, компилятор выдаст ошибку о неучтённом типе.
Заключение
Предложенный подход позволяет обеспечить гибкость и расширяемость вашей системы обработки ошибок. Теперь, когда вы добавите новую ошибку, вам просто нужно убедиться, что реализация интерфейса в библиотеке и соответствующий счётчик обновлены в клиентском коде, без необходимости создания дополнительных проверок на этапе выполнения. Это значительно уменьшает вероятность ошибок и повышает надёжность вашего приложения.