美文网首页后端早读课
在 Go 语言中如何优雅地处理错误

在 Go 语言中如何优雅地处理错误

作者: cd50850d83d8 | 来源:发表于2020-10-23 10:20 被阅读0次

    - 后端早读课翻译计划 第二篇 -

    欢迎关注微信公众号: 后端早读课

    本文提供了一个优雅的处理 Golang 中错误的方法,解决了 Golang error 只有字符串信息的局限性,提供了上下文信息、错误类型判断的功能。

    尽管 go 具有一个简单的错误模型,但是乍一看,事情并没有那么容易。在本文中,提供了一个很好的处理错误的策略并克服您可能遇到的问题。

    首先,我们将分析 go 中的错误是什么。

    然后,我们再看错误创建和处理之间的流程,并分析有可能出现的漏洞。

    Go 的错误类型

    查看内建的错误类型,我们可以得到一些结论:

    // The error built-in interface type is the conventional interface for
    // representing an error condition, with the nil value representing no error.
    //内置错误接口类型用于表示错误状况的普通接口,其中 nil 值表示没有错误。
    type error interface {
        Error() string
    }
    

    我们看到,错误是一个简单的 interface,实现了的 Error 方法,返回一个字符串。

    这个定义告诉我们,创建一个错误只需要一个简单的字符串就可以了,所以如果我创建下面的结构体:

    type MyCustomError string
    func (err MyCustomError) Error() string {
      return string(err)
    }
    

    我就得到了一个最简单的错误定义。

    注意:这里只是举一个例子。我们可以使用 Go 标准库里面的 fmt 和 errors 来创建错误:

    import (
      "errors"
      "fmt"
    )
    simpleError := errors.New("a simple error")
    simpleError2 := fmt.Errorf("an error from a %s string", "formatted")
    

    一段简单的信息能否优雅の处理错误吗?让我们在最后回答这个问题,看看我是怎么做的。

    Error flow 错误处理

    我们已经知道了什么是错误,下一步我们来看看一个错误的生命周期是怎样的。

    简单起见,不要重复自己的错误处理逻辑,最好一个地方只处理一个逻辑。

    通过下面这个例子,我们看看为什么这么说:

    // bad example of handling and returning the error at the same time
    // 错误的示范:在一个地方处理(打印)并返回了错误
    func someFunc() (Result, error) {
     result, err := repository.Find(id)
     if err != nil {
       log.Errof(err)
       return Result{}, err
     }
      return result, nil
    }
    

    这段代码有什么问题吗?

    我们处理两次错误,第一次打印了他,第二次把它返回给调用者。

    也许你的团队同事使用了这个方法,当错误返回时,他会将错误日志再一次的打印出来。在系统里就出现了日志噩梦(多次打印同一个日志)

    想想看我们的应用里有三层,数据层、交互层、Web 服务层:

    // The repository uses an external depedency orm
    func getFromRepository(id int) (Result, error) {
      result := Result{ID: id}
      err := orm.entity(&result)
      if err != nil {
        return Result{}, err
      }
      return result, nil
    }
    

    按照我之前提到的原则,这是一个正确的错误处理方式:把错误返回到最上层。然后他会被打印到日志里。将错误收集反馈在 Web 服务层,只在一个地方处理错误。

    但是这段代码有一个问题。不幸的是, Go 的内置错误没有提供错误栈跟踪。除此之外,这个错误是在外部依赖下生成的,我们需要知道项目中的哪段代码对这个错误负责。

    github.com/pkg/errors 拯救了这个问题。

    我将上面的方法重写,添加堆栈跟踪,以及从数据层获取失败的信息,而且是在不改动原始的错误下:

    import "github.com/pkg/errors"
    // The repository uses an external depedency orm
    func getFromRepository(id int) (Result, error) {
      result := Result{ID: id}
      err := orm.entity(&result)
      if err != nil {
        return Result{}, errors.Wrapf(err, "error getting the result with id %d", id);
      }
      return result, nil
    }
    // after the error wraping the result will be
    // err.Error() -> error getting the result with id 10: whatever it comes from the orm
    

    这个方法做的事儿是:在 ORM 返回的错误外面包装一层,在不影响原始错误的情况下,创建一个堆栈跟踪(译者注:wrap 的嵌套)。

    让我们看下其他层是如何处理这个错误的。交互层:

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

    最顶层的 Web 服务层:

    r := mux.NewRouter()
    r.HandleFunc("/result/{id}", ResultHandler)
    func ResultHandler(w http.ResponseWriter, r *http.Request) {
      vars := mux.Vars(r)
      result, err := interactor.getInteractor(vars["id"])
      if err != nil {
        handleError(w, err)
      }
      fmt.Fprintf(w, result)
    }
    func handleError(w http.ResponseWriter, err error) {
       w.WriteHeader(http.StatusIntervalServerError)
       log.Errorf(err)
       fmt.Fprintf(w, err.Error())
    }
    

    正如你所见,我们只在顶层处理了错误。这样就完美了吗?并不是。如果你注意到,我们在错误的情况下都返回了 HTTP status 500. 除此之外,我们总是记录相同的错误,比如 “result not found” ,这样只会增加我们的日志噪音。

    My Solution 解决方案

    我们在上一个主题中看到,只在顶层处理错误时,简单的字符串错误信息不足以让我们做错误处理的决策。

    我们知道,我们在错误中创建一些信息,我们通常会加入一些信息比如错误是在哪里发生的,错误需要在哪里被处理。

    让我们为解决这个问题定义三个目标:

    • 提供错误栈

    • 打印错误(比如, Web 服务层)

    • 在必要时提供错误的上下文信息(比如,提示电子邮件格式不正确)

    首先,我们创建个错误类型:

    package errors
    const(
      NoType = ErrorType(iota)
      BadRequest
      NotFound
      //add any type you want
    )
    type ErrorType uint
    type customError struct {
      errorType ErrorType
      originalError error
      contextInfo map[string]string
    }
    // Error returns the mssage of a customError
    func (error customError) Error() string {
       return error.originalError.Error()
    }
    // New creates a new customError
    func (type ErrorType) New(msg string) error {
       return customError{errorType: type, originalError: errors.New(msg)}
    }
    
    // New creates a new customError with formatted message
    func (type ErrorType) Newf(msg string, args ...interface{}) error {
       err := fmt.Errof(msg, args...)
    
       return customError{errorType: type, originalError: err}
    }
    
    // Wrap creates a new wrapped error
    func (type ErrorType) Wrap(err error, msg string) error {
       return type.Wrapf(err, msg)
    }
    
    // Wrap creates a new wrapped error with formatted message
    func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {
       newErr := errors.Wrapf(err, msg, args..)
    
       return customError{errorType: errorType, originalError: newErr}
    }
    

    我只定义了public ErrorType 以及错误类型,我们可以创建新的错误,并且可以将已有的错误进行包装。

    但是我们缺少两件事。

    • 如何在不导出 customError 的情况下检查错误类型?

    • 我们如何添加/获取错误的上下文,甚至是一个已存在的来自外部依赖的错误?

    让我们使用 github.com/pkg/errors 提供的策略。首先包装这些库方法:

    // New creates a no type error
    func New(msg string) error {
       return customError{errorType: NoType, originalError: errors.New(msg)}
    }
    
    // Newf creates a no type error with formatted message
    func Newf(msg string, args ...interface{}) error {
       return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}
    }
    
    // Wrap wrans an error with a string
    func Wrap(err error, msg string) error {
       return Wrapf(err, msg)
    }
    
    // Cause gives the original error
    func Cause(err error) error {
       return errors.Cause(err)
    }
    
    // Wrapf wraps an error with format string
    func Wrapf(err error, msg string, args ...interface{}) error {
       wrappedError := errors.Wrapf(err, msg, args...)
       if customErr, ok := err.(customError); ok {
          return customError{
             errorType: customErr.errorType,
             originalError: wrappedError,
             contextInfo: customErr.contextInfo,
          }
       }
       return customError{errorType: NoType, originalError: wrappedError}
    }
    

    添加一些方法来处理上下文和类型来解决已知或者未知错误(NoType)。

    // AddErrorContext adds a context to an error
    func AddErrorContext(err error, field, message string) error {
       context := errorContext{Field: field, Message: message}
       if customErr, ok := err.(customError); ok {
          return customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context}
       }
    
       return customError{errorType: NoType, originalError: err, contextInfo: context}
    }
    
    // GetErrorContext returns the error context
    func GetErrorContext(err error) map[string]string {
       emptyContext := errorContext{}
       if customErr, ok := err.(customError); ok || customErr.contextInfo != emptyContext  {
    
          return map[string]string{"field": customErr.context.Field, "message": customErr.context.Message}
       }
    
       return nil
    }
    
    // GetType returns the error type
    func GetType(err error) ErrorType {
       if customErr, ok := err.(customError); ok {
          return customErr.errorType
       }
    
       return NoType
    }
    

    回到我们的例子,我们将使用这个新的 error 方法

    import "github.com/our_user/our_project/errors"
    // The repository uses an external depedency orm
    func getFromRepository(id int) (Result, error) {
      result := Result{ID: id}
      err := orm.entity(&result)
      if err != nil {
        msg := fmt.Sprintf("error getting the  result with id %d", id)
        switch err {
        case orm.NoResult:
            err = errors.Wrapf(err, msg);
        default:
            err = errors.NotFound(err, msg);  
        }
        return Result{}, err
      }
      return result, nil
    }
    // after the error wraping the result will be
    // err.Error() -> error getting the result with id 10: whatever it comes from the orm
    

    现在的交互层:

    func getInteractor(idString string) (Result, error) {
      id, err := strconv.Atoi(idString)
      if err != nil {
        err = errors.BadRequest.Wrapf(err, "interactor converting id to int")
        err = errors.AddContext(err, "id", "wrong id format, should be an integer)
    
        return Result{}, err
      }
      return repository.getFromRepository(id)
    }
    

    最后的 Web 服务层:

    r := mux.NewRouter()
    r.HandleFunc("/result/{id}", ResultHandler)
    func ResultHandler(w http.ResponseWriter, r *http.Request) {
      vars := mux.Vars(r)
      result, err := interactor.getInteractor(vars["id"])
      if err != nil {
        handleError(w, err)
      }
      fmt.Fprintf(w, result)
    }
    func handleError(w http.ResponseWriter, err error) {
       var status int
       errorType := errors.GetType(err)
       switch errorType {
         case BadRequest:
          status = http.StatusBadRequest
         case NotFound:
          status = http.StatusNotFound
         default:
          status = http.StatusInternalServerError
       }
       w.WriteHeader(status)
    
       if errorType == errors.NoType {
         log.Errorf(err)
       }
       fmt.Fprintf(w,"error %s", err.Error())
    
       errorContext := errors.GetContext(err)
       if errorContext != nil {
         fmt.Printf(w, "context %v", errorContext)
       }
    }
    

    如你所见,通过导出类型和一些导出的值,我们可以让处理错误的生活更容易一点。在这个解决方案里的设计中,有一点我非常喜欢,就是在创建错误的时候我们明确了错误的具体类型。

    阅读原文

    欢迎关注微信公众号: 后端早读课

    相关文章

      网友评论

        本文标题:在 Go 语言中如何优雅地处理错误

        本文链接:https://www.haomeiwen.com/subject/jftvmktx.html