博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译]Go如何优雅的处理异常
阅读量:6989 次
发布时间:2019-06-27

本文共 7956 字,大约阅读时间需要 26 分钟。

原文:

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

正文

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

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

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

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

什么是error?

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

// error类型在内置包中的定义是一个简单的接口// 其中nil代表没有异常type error interface {    Error() string}复制代码

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

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

type MyCustomError stringfunc (err MyCustomError) Error() string {  return string(err)}复制代码

  下面我们用标准包fmterrors去声明一些error

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

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

error处理流

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

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

  我们来看下以下的例子:

// 同时进行error处理和返回error// 这是一种糟糕的写法func someFunc() (Result, error) {    result, err := repository.Find(id)    if err != nil {        log.Errof(err)        return Result{}, err    }    return result, nil}复制代码

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

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

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

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

// 数据层使用了一个第三方orm库func getFromRepository(id int) (Result, error) {    result := Result{ID: id}    err := orm.entity(&result)    if err != nil {        return Result{}, err    }    return result, nil }复制代码

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

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

  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 }// 当error封装完后,返回的error信息将会是这样的// err.Error() -> error getting the result with id 10// 这就很容易知道这是来自orm库的error了复制代码

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

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

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)}复制代码

  接着是接口层:

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) {    // 返回HTTO 500错误    w.WriteHeader(http.StatusIntervalServerError)    log.Errorf(err)    fmt.Fprintf(w, err.Error())}复制代码

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

解决方法

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

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

error处理的三个宗旨

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

我们来创建一个error类型:

const(  NoType = ErrorType(iota)  BadRequest  NotFound  // 可以加入你需要的error类型)type ErrorType uinttype customError struct {  errorType ErrorType  originalError error  contextInfo map[string]string }// 返回customError具体的错误信息func (error customError) Error() string {   return error.originalError.Error()}// 创建一个新的customErrorfunc (type ErrorType) New(msg string) error {   return customError{errorType: type, originalError: errors.New(msg)}}// 给customError自定义错误信息func (type ErrorType) Newf(msg string, args ...interface{}) error {   err := fmt.Errof(msg, args...)      return customError{errorType: type, originalError: err}}// 对error进行封装func (type ErrorType) Wrap(err error, msg string) error {   return type.Wrapf(err, msg)}// 对error进行封装,并加入格式化信息func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {   newErr := errors.Wrapf(err, msg, args..)      return customError{errorType: errorType, originalError: newErr}}复制代码

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

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

// 创建一个NoType errorfunc New(msg string) error {   return customError{errorType: NoType, originalError: errors.New(msg)}}// 创建一个加入了格式化信息的NoType errorfunc Newf(msg string, args ...interface{}) error {   return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}}// 给error封装多一层stringfunc Wrap(err error, msg string) error {   return Wrapf(err, msg)}// 返回最原始的errorfunc Cause(err error) error {   return errors.Cause(err)}// error加入格式化信息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}}复制代码

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

// AddErrorContext adds a context to an errorfunc 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 contextfunc 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 typefunc GetType(err error) ErrorType { if customErr, ok := err.(customError); ok { return customErr.errorType } return NoType}复制代码

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

import "github.com/our_user/our_project/errors"// The repository uses an external depedency ormfunc 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)}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)   }}复制代码

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

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

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

转载于:https://juejin.im/post/5cee8ec1518825477a52dee8

你可能感兴趣的文章
Cap12_项目采购管理
查看>>
ptmalloc2源码解析初探
查看>>
用为知笔记发博客
查看>>
[转] WINCC教学视频
查看>>
POJ题目(转)
查看>>
maven 依赖范围
查看>>
HDU - 1520 Anniversary party [树形dp]
查看>>
nginx搭建多个站点
查看>>
字符串运算符
查看>>
linux大全
查看>>
jq 鼠标旋转控制也能3d旋转
查看>>
Spring+Mybatis多数据库的配置
查看>>
线性表----单链表
查看>>
mysql 启动报错
查看>>
Git 与 GitHub
查看>>
Spring Cloud:多环境配置、eureka 安全认证、容器宿主机IP注册
查看>>
【leetcode】667. Beautiful Arrangement II
查看>>
【leetcode】955. Delete Columns to Make Sorted II
查看>>
JDK源码阅读-Integer
查看>>
Java修行之路
查看>>