[译]Go如何优雅的处理异常

https://mp.weixin.qq.com/s/GEWy8AQg5WmlX3POearpZQ
Joy Go后端干货 Go后端干货 微信号 功能介绍 后端及Go,干货分享! 4天前

原文:https://hackernoon.com/golang-handling-errors-gracefully-8e27f1db729f

注:译文中 error 可以理解为异常,但Go中的 error 和Java中的异常还是有很大区别的,需要读者慢慢体会,所以为了方便阅读和思考,译文中的名词 error 就不翻译了。

正文

Go有一套简单的 error 处理模型,但其实并不像看起来那么简单。本文中,我会提供一种好的方法去处理 error ,并用这个方法来解决在往后编程遇到的的类似问题。

首先,我们会分析下Go中的 error

接着我们来看看 error 的产生和 error 的处理,再分析其中的缺陷。

最后,我们将要探索一种方法来解决我们在程序中遇到的类似问题。

什么是error?

看下 error 在内置包中的定义,我们可以得出一些结论:

  1. // error类型在内置包中的定义是一个简单的接口
  2. // 其中nil代表没有异常
  3. type error interface {
  4. Error() string
  5. }

从上面的代码,我们可以看到 error 是一个接口,只有一个 error 方法。

那我们要实现error就很简单了,看以下代码:

  1. type MyCustomError string
  2. func (err MyCustomError) Error() string {
  3. return string(err)
  4. }

下面我们用标准包fmt和errors去声明一些 error

  1. import (
  2. "errors"
  3. "fmt"
  4. )
  5. simpleError := errors.New("a simple error")
  6. simpleError2 := fmt.Errorf("an error from a %s string", "formatted")

思考:上面的 error 定义中,只有这些简单的信息,就足够处理好异常吗?我们先不着急回答,下面我们去寻找一种好的解决方法。

error处理流

现在我们已经知道了在Go中的 error 是怎样的了,下一步我们来看下 error 的处理流程。

为了遵循简约和DRY(避免重复代码)原则,我们应该只在一个地方进行 error 的处理。

我们来看下以下的例子:

  1. // 同时进行error处理和返回error
  2. // 这是一种糟糕的写法
  3. func someFunc() (Result, error) {
  4. result, err := repository.Find(id)
  5. if err != nil {
  6. log.Errof(err)
  7. return Result{}, err
  8. }
  9. return result, nil
  10. }

上面这段代码有什么问题呢?

我们首先打印了这个 error 信息,然后又将 error 返回给函数的调用者,这相当于重复进行了两次 error 处理。

**很有可能你组里的同事会用到这个方法,当出现 error 时,他很有可能又会将这个 error 打印一遍,然后重复的日志就会出现在系统日志里了。

我们先假设程序有3层结构,分别是数据层,交互层和接口层:

  1. // 数据层使用了一个第三方orm库
  2. func getFromRepository(id int) (Result, error) {
  3. result := Result{ID: id}
  4. err := orm.entity(&result)
  5. if err != nil {
  6. return Result{}, err
  7. }
  8. return result, nil
  9. }

根据DRY原则,我们可以将 error 返回给调用的最上层接口层,这样我们就能统一的对 error 进行处理了。

但上面的代码有一个问题,Go的内置 error 类型是没有调用栈的。另外,如果 error 产生在第三方库中,我们还需要知道我们项目中的哪段代码负责了这个 error

github.com/pkg/errors 可以使用这个库来解决上面的问题。

利用这个库,我对上面的代码进行了一些改进,加入了调用栈和加了一些相关的错误信息。

  1. import "github.com/pkg/errors"
  2. // 使用了第三方的orm库
  3. func getFromRepository(id int) (Result, error) {
  4. result := Result{ID: id}
  5. err := orm.entity(&result)
  6. if err != nil {
  7. return Result{}, errors.Wrapf(err, "error getting the result with id %d", id);
  8. }
  9. return result, nil
  10. }
  11. // 当error封装完后,返回的error信息将会是这样的
  12. // err.Error() -> error getting the result with id 10
  13. // 这就很容易知道这是来自orm库的error了

上面的代码对orm的 error 进行了封装,增加了调用栈,而且没有修改原始的 error 信息。

然后我们再来看看在其他层是如何处理这个 error 的,首先是交互层:

  1. func getInteractor(idString string) (Result, error) {
  2. id, err := strconv.Atoi(idString)
  3. if err != nil {
  4. return Result{}, errors.Wrapf(err, "interactor converting id to int")
  5. }
  6. return repository.getFromRepository(id)
  7. }

接着是接口层:

  1. func ResultHandler(w http.ResponseWriter, r *http.Request) {
  2. vars := mux.Vars(r)
  3. result, err := interactor.getInteractor(vars["id"])
  4. if err != nil {
  5. handleError(w, err)
  6. }
  7. fmt.Fprintf(w, result)
  8. }
  9. func handleError(w http.ResponseWriter, err error) {
  10. // 返回HTTO 500错误
  11. w.WriteHeader(http.StatusIntervalServerError)
  12. log.Errorf(err)
  13. fmt.Fprintf(w, err.Error())
  14. }

现在我们只在最上层接口层处理了 error ,看起来很完美?并不是,如果程序中经常返回HTTP错误码500,同时将错误打印到日志中,像result not found这种没用的日志就会很烦人。

解决方法

我们上面讨论到仅仅靠一个字符串是不足以处理好 error 的。我们也知道通过给 error 加一些额外的信息就能追溯到 error 的产生和最后的处理逻辑。

因此我定义了三个 error 处理的宗旨。

error处理的三个宗旨

  • 提供清晰完整的调用栈
  • 必要时提供 error 的上下文信息
  • 打印 error 到日志中(例如可以在框架层打印)

我们来创建一个 error 类型:

  1. const(
  2. NoType = ErrorType(iota)
  3. BadRequest
  4. NotFound
  5. // 可以加入你需要的error类型
  6. )
  7. type ErrorType uint
  8. type customError struct {
  9. errorType ErrorType
  10. originalError error
  11. contextInfo map[string]string
  12. }
  13. // 返回customError具体的错误信息
  14. func (error customError) Error() string {
  15. return error.originalError.Error()
  16. }
  17. // 创建一个新的customError
  18. func (type ErrorType) New(msg string) error {
  19. return customError{errorType: type, originalError: errors.New(msg)}
  20. }
  21. // 给customError自定义错误信息
  22. func (type ErrorType) Newf(msg string, args ...interface{}) error {
  23. err := fmt.Errof(msg, args...)
  24. return customError{errorType: type, originalError: err}
  25. }
  26. // 对error进行封装
  27. func (type ErrorType) Wrap(err error, msg string) error {
  28. return type.Wrapf(err, msg)
  29. }
  30. // 对error进行封装,并加入格式化信息
  31. func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {
  32. newErr := errors.Wrapf(err, msg, args..)
  33. return customError{errorType: errorType, originalError: newErr}
  34. }

从上面的代码可以看到,我们可以创建一个新的 error 类型或者对已有的 error 进行封装。但我们遗漏了两件事情,一是我们不知道 error 的具体类型。二是我们不知道怎么给这这个 error 加上下文信息。

为了解决以上问题,我们来对github.com/pkg/errors的方法也进行一些封装。

  1. // 创建一个NoType error
  2. func New(msg string) error {
  3. return customError{errorType: NoType, originalError: errors.New(msg)}
  4. }
  5. // 创建一个加入了格式化信息的NoType error
  6. func Newf(msg string, args ...interface{}) error {
  7. return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}
  8. }
  9. // 给error封装多一层string
  10. func Wrap(err error, msg string) error {
  11. return Wrapf(err, msg)
  12. }
  13. // 返回最原始的error
  14. func Cause(err error) error {
  15. return errors.Cause(err)
  16. }
  17. // error加入格式化信息
  18. func Wrapf(err error, msg string, args ...interface{}) error {
  19. wrappedError := errors.Wrapf(err, msg, args...)
  20. if customErr, ok := err.(customError); ok {
  21. return customError{
  22. errorType: customErr.errorType,
  23. originalError: wrappedError,
  24. contextInfo: customErr.contextInfo,
  25. }
  26. }
  27. return customError{errorType: NoType, originalError: wrappedError}
  28. }

接着我们给 error 加入上下文信息:

  1. // AddErrorContext adds a context to an error
  2. func AddErrorContext(err error, field, message string) error {
  3. context := errorContext{Field: field, Message: message}
  4. if customErr, ok := err.(customError); ok {
  5. return customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context}
  6. }
  7. return customError{errorType: NoType, originalError: err, contextInfo: context}
  8. }
  9. // GetErrorContext returns the error context
  10. func GetErrorContext(err error) map[string]string {
  11. emptyContext := errorContext{}
  12. if customErr, ok := err.(customError); ok || customErr.contextInfo != emptyContext {
  13. return map[string]string{"field": customErr.context.Field, "message": customErr.context.Message}
  14. }
  15. return nil
  16. }
  17. // GetType returns the error type
  18. func GetType(err error) ErrorType {
  19. if customErr, ok := err.(customError); ok {
  20. return customErr.errorType
  21. }
  22. return NoType
  23. }

现在将上述的方法应用在我们文章开头写的example中:

  1. import "github.com/our_user/our_project/errors"
  2. // The repository uses an external depedency orm
  3. func getFromRepository(id int) (Result, error) {
  4. result := Result{ID: id}
  5. err := orm.entity(&result)
  6. if err != nil {
  7. msg := fmt.Sprintf("error getting the result with id %d", id)
  8. switch err {
  9. case orm.NoResult:
  10. err = errors.Wrapf(err, msg);
  11. default:
  12. err = errors.NotFound(err, msg);
  13. }
  14. return Result{}, err
  15. }
  16. return result, nil
  17. }
  18. // after the error wraping the result will be
  19. // err.Error() -> error getting the result with id 10: whatever it comes from the orm
  1. func getInteractor(idString string) (Result, error) {
  2. id, err := strconv.Atoi(idString)
  3. if err != nil {
  4. err = errors.BadRequest.Wrapf(err, "interactor converting id to int")
  5. err = errors.AddContext(err, "id", "wrong id format, should be an integer)
  6. return Result{}, err
  7. }
  8. return repository.getFromRepository(id)
  9. }
  1. func ResultHandler(w http.ResponseWriter, r *http.Request) {
  2. vars := mux.Vars(r)
  3. result, err := interactor.getInteractor(vars["id"])
  4. if err != nil {
  5. handleError(w, err)
  6. }
  7. fmt.Fprintf(w, result)
  8. }
  9. func handleError(w http.ResponseWriter, err error) {
  10. var status int
  11. errorType := errors.GetType(err)
  12. switch errorType {
  13. case BadRequest:
  14. status = http.StatusBadRequest
  15. case NotFound:
  16. status = http.StatusNotFound
  17. default:
  18. status = http.StatusInternalServerError
  19. }
  20. w.WriteHeader(status)
  21. if errorType == errors.NoType {
  22. log.Errorf(err)
  23. }
  24. fmt.Fprintf(w,"error %s", err.Error())
  25. errorContext := errors.GetContext(err)
  26. if errorContext != nil {
  27. fmt.Printf(w, "context %v", errorContext)
  28. }
  29. }

通过简单的封装,我们可以明确的知道 error 的错误类型了,然后我们就能方便进行处理了。

读者也可以将代码运行一遍,或者利用上面的errors库写一些demo来加深理解。

感谢阅读,欢迎大家指正,留言交流~

ft_authoradmin  ft_create_time2019-06-02 13:31
 ft_update_time2019-06-02 13:31