[Go 教程] 如何优雅的 Golang 错误处理

虽然 go 语言中有一个简单的错误模型,看似容易,实则大有文章。本文中, 我将为诸位献计献策,进行错误处理,拔除芒刺。

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

然后我们将学习错误创建和错误处理之间的流程,并分析可能的瑕疵。

终了,我们将因时施宜找出一种无损于应用设计的方案来解决瑕疵。

在 go 中错误是怎么定义的

通过查看 go 语言内置的错误类型,我们可以得出一些结论:

// 内置的error接口,是一个常规的用于处理错误的接口。
// 其中 nil 表示没有错误。
type error interface {
    Error() string
}

可以看到,一个错误处理其实就是一个接口,这接口中包含了一个简单的 Error() 方法,该方法的返回值是字符串。

这个定义告诉我们实现一个错误处理,所需要的只是给 Error() 方法返回一个简单的字符串,
所以,如果我创建以下结构:

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

上面的代码中,我们实现了最简单的错误处理。

Note:这只是举个例子。 我们可以使用 go 标准包 fmt 或者 errors 创建错误:

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

一条这么简单的信息能够优雅地处理错误吗?让我们最后来回答这个问题,接下来,讨论下我将要提供的一些解决方案。

错误处理流程

我们已经知道什么是错误,下一步是可视化他生命周期中的流程。

为了简单起见,不要重复你自己的原则,最好在一个地方对一个错误采取一次行动。

让我们看看为什么给出下面的例子:

// 同时处理和返回错误的坏例子
func someFunc() (Result, error) {
 result, err := repository.Find(id)
 if err != nil {
   log.Errof(err)
   return Result{}, err
 }
  return result, nil
}

这段代码有什么问题?

我们先记录错误,然后将它返回给这个函数的调用方,这样就处理了两次错误。

可能您团队的一个同事将使用此方法,当错误返回时,他将再次记录错误。 然后在系统日志中会发生噩梦般的错误记录。

因此,想像我们的应用程序有 3 层,存储层交互层Web 服务器层

// 这个存储层使用外部依赖的 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"
// 这个存储层使用外部依赖的 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 
}
// 封装错误后的结果将是
// err.Error() -> error getting the result with id 10: whatever it comes from the orm

这个函数的作用是,在不影响原始错误的前提下构建堆栈追踪,封装来自 orm 的错误。

所以让我们来看看其他层是如何处理错误的。首先是交互层:

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())
}

如你所见,我们仅仅在顶层处理了错误。这样完美吗?当然不,如果你注意,你会发现我们总是会接收到返回500的 HTTP 状态码。此外,我们总是记录错误。像 「result not found」的一些错误只会给日志增加垃圾信息。

我的方案

在上一个主题中,我们发现在处理顶层错误时,一个字符串不足以让我们做出决策。

而且,我们清楚如果我们在错误中引入新的内容,我们将在创建错误和最后处理错误的时候产生一个依赖。

那么让我们来探讨一下,这个定义了三个目标的解决方案:

  • 提供良好的错误堆栈跟踪
  • 记录错误 (例如, Web 基础结构层)
  • 必要时向用户提供上下文错误信息。 (例如, 提供的电子邮件格式不对)

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

package errors
const(
  NoType = ErrorType(iota)
  BadRequest
  NotFound 
  //增加任何你想要的类型
)
type ErrorType uint
type customError struct {
  errorType ErrorType 
  originalError error 
  contextInfo map[string]string 
}
// Error 方法返回一个 customError 消息
func (error customError) Error() string {
   return error.originalError.Error()
}
// New 方法新建一个新的 customError 对象
func (type ErrorType) New(msg string) error {
   return customError{errorType: type, originalError: errors.New(msg)}
}

// Newf 方法使用格式化消息新建 customError 对象
func (type ErrorType) Newf(msg string, args ...interface{}) error {    
   err := fmt.Errof(msg, args...)

   return customError{errorType: type, originalError: err}
}

// Wrap 方法新建一个封装错误
func (type ErrorType) Wrap(err error, msg string) error {
   return type.Wrapf(err, msg)
}

// Wrapf 方法使用格式化消息创建新的封装错误
func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error { 
   newErr := errors.Wrapf(err, msg, args..)   

   return customError{errorType: errorType, originalError: newErr}
}

因此,您可能会看到我只公开 ErrorType 和错误类型。 我们可以创建新错误并封装现有错误。

但是我们忽略了两个事情。

  • 如果没有导出 customError 对像,我们怎么检查错误类型?
  • 对于外部依赖项中预先存在的错误,我们如何增加/获取错误的内容?

让我们采用 github.com/pkg/errors. 的策略,首先封装这些库方法。

// New 方法新建一个错误类型
func New(msg string) error {
   return customError{errorType: NoType, originalError: errors.New(msg)}
}

// Newf 方法用格式化消息新建了一个错误类型
func Newf(msg string, args ...interface{}) error {
   return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}
}

// Wrap 方法用字符串封装错误
func Wrap(err error, msg string) error {
   return Wrapf(err, msg)
}

// Cause 方法返回原始错误
func Cause(err error) error {
   return errors.Cause(err)
}

// Wrapf 方法用格式化字符串封装错误
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}
}

现在让我们构建我们的方法来处理任何一般错误的上下文和类型:

// AddErrorContext 方法为错误添加上下文
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 方法返回错误内容
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 方法返回错误类型
func GetType(err error) ErrorType {
   if customErr, ok := err.(customError); ok {
      return customErr.errorType
   }

   return NoType
}

现在回到我们的示例,我们将应用这个新的错误包:

import "github.com/our_user/our_project/errors"
// 存储库使用外部依赖项 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 
}
// 错误包装后结果将是
// err.Error() -> 错误获取 id 为 10 的结果:无论它来自 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)
   }
}

正如你可能看到的那样,通过一个导出类型和一些导出值,我们可以轻松的处理错误。在这个解决方案的设计中,我最喜欢的一点是:当创建一个错误时,我们明确的显示了它的类型。

你是否有其他建议? 请在下面链接评论。

github repositoryhttps://github.com/henrmota/errors-handlin...

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://hackernoon.com/golang-handling-e...

译文地址:https://learnku.com/go/t/33210

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 2
drinke9

学习了,不错的文章

1年前 评论

go写多了就会觉得错误处理是真的麻烦,每一个函数调用都要处理一次,非常繁琐

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!