前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 进阶训练营 – 错误处理一:最佳实践

Go 进阶训练营 – 错误处理一:最佳实践

作者头像
Yuyy
发布2022-09-21 10:31:10
9320
发布2022-09-21 10:31:10
举报

同系列文章:Go 进阶训练营

Go error/panic VS java exception

和java相比,go的异常处理两极化,panic比exception更严重,java exception是线程级别的,而go的panic是进程级别,任意goroutine出现panic都会导致整个进程挂掉,更能提醒异常情况。error比exception更轻微,在go中,error是当做值来处理的,更加灵活、细致,但需要大量的if err!=nil(考验代码功底的时候到了)。而exception的全局异常捕获用起来更方便、笼统。整体来讲,各有利弊(废话,要是绝对碾压就不会都存在了)

panic

在程序启动的时候,如果有强依赖的服务出现故障时 panic 退出

在程序启动的时候,如果发现有配置明显不符合要求, 可以 panic 退出(防御编程)

其他情况下只要不是不可恢复的程序错误,都不应该直接 panic 应该返回 error

在程序入口处,例如 gin 中间件需要使用 recover 预防 panic 程序退出

在程序中我们应该避免使用野生的goroutine

如果是在请求中需要执行异步任务,应该使用异步 worker ,消息通知的方式进行处理,避免请求量大时大量 goroutine 创建。

如果需要使用 goroutine 时,应该使用统一的 Go 函数进行创建,这个函数中会进行 recover ,避免因为野生 goroutine panic 导致主进程退出。

代码语言:javascript
复制
func Go(f func()){
  go func(){
      defer func(){
          if r := recover(); r != nil {
            buf := make([]byte, 64<<10)
            buf = buf[:runtime.Stack(buf, false)]
            err = fmt.Errorf("panic recovered: %s\n%s", r, buf)
        }
        if err!=nil {
            log.Printf("panic: %+v", err)
        }
      }()

      f()
  }()
}

error

我们在应用程序中使用 github.com/pkg/errors 处理应用错误,注意在公共库当中,我们一般不使用这个

error 应该是函数的最后一个返回值,当 error 不为nil 时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待

  1. func f() (io.Reader, *S1, error) 在这里,我们不知道 io.Reader 中是否有数据,可能有,也有可能有一部分

错误处理的时候应该先判断错误, if err != nil 出现错误及时返回,使代码是一条流畅的直线,避免过多的嵌套。也就是使用谓语句。

如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用 errors.WithMessage

如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用errors.Wrap添加堆栈信息

  1. 切记,不要每个地方都是用 errors.Wrap 只需要在错误第一次出现时进行 errors.Wrap 即可
  2. 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 repository 层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码
  3. 注意我们在基础库,被大量引入的第三方库编写时一般不使用 errors.Wrap 避免堆栈信息重复

禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用 %+v 进行统一打印,例如 http/rpc 服务的中间件。

错误判断使用 errors.Is 进行比较。

  1. 判断error类型时,不应直接使用==,1、err如果是指针,就不会想等。2、err如果包裹过errors.Wrap(),也会不相等。应该使用errors.As
代码语言:javascript
复制
func Is(err, target error) bool {
if target == nil {
    return err == target
}

isComparable := reflectlite.TypeOf(target).Comparable()
for {
    if isComparable && err == target {
        return true
    }
    if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
        return true
    }
    // TODO: consider supporting target.Is(err). This would allow
    // user-definable predicates, but also may allow for coping with sloppy
    // APIs, thereby making it easier to get away with them.
    if err = Unwrap(err); err == nil {
        return false
    }
}
}

reflectlite.TypeOf(target).Comparable():先判断是否可通过==比较

  • Comparable():判断equal属性是否为空,equal是一个方法。
代码语言:javascript
复制
func (t *rtype) Comparable() bool {
return t.equal != nil
}

type rtype struct {
size       uintptr
ptrdata    uintptr // number of bytes in the type that can contain pointers
hash       uint32  // hash of type; avoids computation in hash tables
tflag      tflag   // extra type information flags
align      uint8   // alignment of variable with this type
fieldAlign uint8   // alignment of struct field with this type
kind       uint8   // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal     func(unsafe.Pointer, unsafe.Pointer) bool
gcdata    *byte   // garbage collection data
str       nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
  • 指针变量默认比较两个指针内存地址。
image-20220806101750565
image-20220806101750565
  • 仅包含string字段的结构体变量,equal为runtime.strequal
image-20220806124903356
image-20220806124903356

《go语言精进之路》里string章节详细讲了string比较,后面会分享笔记。

  • 包含多种类型的结构体变量,equal为type..eq.main.MyError
代码语言:javascript
复制
type MyError struct {
a   int
msg string
}
image-20220806125153472
image-20220806125153472

结构体变量比较:结构体字段逐个比较。推测这里的的equal表示这个意思。

  • map、slience 不支持==比较,可以使用反射包里的reflect.DeepEqual,不过性能差一些。如果是单元测试,可使用google/go-cmp。无法比较时会panic,故非测试代码使用时要注意。

吐槽下go的==比较,是运行时go根据具体类型做不同的操作,没有在源码中体现。而java的比较更清晰,例如字符串比较,可以看到string源码重写的equals方法。

if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target):断言是否实现Is(error) bool接口,实现就直接调用。

err = Unwrap(err):递归解开error。有时需要通过err传递更多信息,就会对error进行包裹。

代码语言:javascript
复制
func Unwrap(err error) error {
u, ok := err.(interface {
    Unwrap() error
})
if !ok {
    return nil
}
return u.Unwrap()
}

错误类型判断,使用 errors.As 进行赋值。

errors.As(a,b):通过反射判断a是否和b是一个类型,如果是就把b赋值给a,如果不是,unwrap a,重复前面的步骤。

对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档。

重视业务错误

panic or error?

  1. 在 Go 中 panic 会导致程序直接退出,是一个致命的错误,如果使用panic recover 进行处理的话,会存在很多问题
    1. 性能问题,频繁 panic recover 性能不好
    2. 容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
    3. 不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理
  2. 什么时候使用 panic 呢?
    1. 对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用 panic
  3. 使用 error 处理有哪些好处?
    1. 简单。
    2. 考虑失败,而不是成功(Plan for failure, not success)。
    3. 没有隐藏的控制流(例如java的全局异常处理)。
    4. 完全交给你来控制 error。
    5. Error are values(这篇文章启发挺大,之前一直没明白go这样处理error的目的)。

参考

  1. 这可能是最全的golang的"=="比较规则了吧
  2. Go错误处理最佳实践
  3. Go 1.13中的错误处理
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-8-06 1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go error/panic VS java exception
  • panic
  • error
  • panic or error?
  • 参考
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档