Что вы делаете, когда что-то идет не так?

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

В GO условия ошибки возвращаются как значения, возвращаемые методом. На мой взгляд, полезно рассматривать условия ошибки как часть основного потока - это возлагает на разработчиков ответственность за обработку ошибок при написании функционального кода. Эта парадигма сильно отличается от того, что предлагают другие языки программирования (например, Java), где исключения представляют собой совершенно другой поток. Хотя этот другой стиль делает код более читабельным, он также создает новые проблемы.

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

Итак, приступим к списку:

По левому краю

Лучшая стратегия для обработки ошибки - это проверить ошибку и немедленно вернуться из функции. Допускается наличие в функции нескольких операторов возврата ошибок - на самом деле, это разумный выбор. [1]

Например, следующий фрагмент кода показывает, как обработка удачного сценария с использованием if err == nil приводит к вложенным проверкам if.

// Handling Happy case first - leading to nested if checks...
func example() error {
     err := somethingThatReturnsError()
     if err == nil {
        //Happy processing
        err = somethingElseThatReturnsError()
        if err == nil {
           //More Happy processing
        } else {
           return err
        }
     } else {
        return err
     }
}

Вышеупомянутую логику можно выровнять по левому краю, выровняв логику по левому краю:

func ABetterExample() error {
     err := somethingThatReturnsError()
     if err != nil {
        return err
     }
     
     // Happy processing
     err = somethingElseThatReturnsError()
     if err != nil {
        return err
     }
     // More Happy processing
}

Повторить попытку исправимых ошибок

Несколько исправимых ошибок заслуживают повторных попыток - сбои в сети, операции ввода-вывода и т. Д. Можно исправить с помощью простых повторных попыток.

Следующий пакет может помочь решить проблему с помощью повторных попыток.



// An operation that may fail.
operation := func() error {
    return nil // or an error
}

err := Retry(operation, NewExponentialBackOff())
if err != nil {
    // Handle error.
    return
}

Экспоненциальный откат означает, что интервал повтора увеличивается экспоненциально - разумный выбор для большинства сбоев сети / ввода-вывода.

Ошибки упаковки

Пакет ошибок по умолчанию ограничен - детали контекста ошибки часто могут быть потеряны. Например,

func testingError2() error {
   return errors.New("New Error")
}
func testingError(accountNumber string) error {
   err := testingError2()
   if err != nil {
      return err
   }
   return nil
}
func main() {
   err := testingError("Acct1")
   logrus.Error("Error occurred", fmt.Sprintf("%+v", err))
}

В этом случае экземпляр ошибки, полученный функцией main, не содержит информации о том, что это произошло для учетной записи Acct1. Можно регистрировать accountNumber в функции testingErrror, но с текущим пакетом errors невозможно передать эту информацию функции main.

Именно здесь на помощь приходит github.com/pkg/errors. Эта библиотека совместима с errors, но имеет некоторые интересные возможности.

func testingError2() error {
   return errors.New("New Error")
}
func testingError(accountNumber string) error {
   err := testingError2()
   if err != nil {
        return errors.Wrap(err, "Error occurred while processing Card Number "+accoutNumber)
   }
   return nil
}
func main() {
   err := testingError("Acct1")
   logrus.Error("Error occurred", fmt.Sprintf("%+v", err))
}

С github.com/pkg/errors вы также можете использовать некоторые дополнительные полезные функции - errors.Unwrap и errors.Is

Стратегии ведения журнала

В пакете Golang по умолчанию log не предусмотрена возможность ведения журнала с указанием уровня ведения журнала. Есть много других вариантов:

Logru s и Zap a также предоставляют возможность структурировать вывод журнала - это очень удобная возможность, поскольку предоставляет разработчикам возможность добавлять контекст в сообщение журнала ошибок.

func example(accountNumber string) error {
 logrus.SetFormatter(&logrus.JSONFormatter{})
ctxFields := logrus.Fields{
  "accountNumber": accountNumber,
  "appname":       "my-app",
 }
//Happy processing
 err := errors.New("Some test error while doing happy processing")
if err != nil {
  logrus.WithFields(ctxFields).WithError(err).Error("ErrMsg")
  return err
 }
 return nil
}

Вывод структурированного журнала будет выглядеть так:

{"accountNumber":"ABC","appname":"my-app","error":"Some test error while doing happy processing","level":"error","msg":"ErrMsg","time":"2009-11-10T23:00:00Z"}

Другой ключевой аспект ведения журнала - это возможность получить трассировку стека журнала. Если вы используете github.com/pkg/errors, вы могли бы

logrus.Error("Error occurred", fmt.Sprintf("%+v", err))

И вы получите трассировку стека ошибок, как показано ниже:

main.testingError2
        /home/nayars/go/src/github.com/nayarsn/temp.go:12
main.testingError
        /home/nayars/go/src/github.com/nayarsn/temp.go:25
main.main
        /home/nayars/go/src/github.com/nayarsn/temp.go:39
runtime.main
        /usr/lib/go-1.15/src/runtime/proc.go:204
runtime.goexit
        /usr/lib/go-1.15/src/runtime/asm_amd64.s:1374

Zap буферизован и оптимизирован для повышения производительности. [2]

Проверки ошибок

Относиться к error как к значению - это хорошо - это явное и явное значение имеет большой смысл. Но он также может дать разработчикам возможность пропустить. Например,

func testingError(accoutNumber string) error {
    var err error
    _ = errors.New("errors.New with _"
    errors.New("errors.New not capturing return")
    return err
}

В приведенном выше примере показано, что прикладной программист получает две ошибки, возвращаемые операторами errors.New. Причем это может произойти намеренно или ненамеренно.

К счастью, есть утилита линтера, которая может вам помочь.



После установки линтера вы можете просто сделать следующее:

errcheck -blank ./...

И вы получите такой результат:

temp.go:16:2:   _ = errors.New("Error capturing return using _")
temp.go:18:12:  errors.New("Error not capturing return")

Это можно добавить как часть конвейера CD CI, чтобы разработчики приложений не пропустили эту часть.

errchec входит в состав утилиты агрегатора Go Linters - https://golangci-lint.run/

Множественные ошибки

У вас есть сценарии, когда у вас есть несколько ошибок - они являются частью той же процедуры, и вы не хотите останавливать обработку - а, скорее, продолжить обработку и записать все ошибки. Для этого есть библиотека:



Вот простой пример:

func step1() error {
    return errors.New("Step1")
}
func step2() error {
    return errors.New("Step2")
}
func main() {
    var result error
    if err := step1(); err != nil {
        result = multierror.Append(result, err)
    }
    if err := step2(); err != nil {
        result = multierror.Append(result, err)
    }
    
    fmt.Println(multierror.Flatten(result))
}

Точно так же для нескольких подпрограмм go можно использовать следующую библиотеку:



Заключение

Я знаю, что приведенный выше список не является исчерпывающим. И для некоторых из вас это было бы все тривиально, но, надеюсь, для некоторых из вас это внесло свой вклад в ваш репертуар методов обработки ошибок. Прокомментируйте, пожалуйста, в «Разделе комментариев», если у вас есть другие полезные идеи.

Ссылки

[1] https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88

[2] https://medium.com/a-journey-with-go/go-how-zap-package-is-optimized-dbf72ef48f2d