同系列文章: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 导致主进程退出。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() }() }
panic需要去recover,是否和panic的意图冲突
在gRPC中,主线程都没有处理panic,个人认为野生协程也没必要处理。如果服务比较重要,不希望一个bug导致服务挂掉,可以利用k8s来保证可用性。换个角度,是不是应该对gRPC做panic保护?
在gin框架中,如果主线程处理了panic(自定义中间件),那就需要处理野生协程的panic。但是在gin使用panic+中间件recover的方式,是违背go作者的意图的,个人不推荐。 -
error
-
我们在应用程序中使用
github.com/pkg/errors
处理应用错误,注意在公共库当中,我们一般不使用这个 -
error 应该是函数的最后一个返回值,当 error 不为nil 时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待
func f() (io.Reader, *S1, error)
在这里,我们不知道io.Reader
中是否有数据,可能有,也有可能有一部分
-
错误处理的时候应该先判断错误,
if err != nil
出现错误及时返回,使代码是一条流畅的直线,避免过多的嵌套。也就是使用谓语句。 -
如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用
errors.WithMessage
-
如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用errors.Wrap添加堆栈信息
- 切记,不要每个地方都是用
errors.Wrap
只需要在错误第一次出现时进行errors.Wrap
即可 - 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把
repository
层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换ORM
库时需要去修改上层代码 - 注意我们在基础库,被大量引入的第三方库编写时一般不使用
errors.Wrap
避免堆栈信息重复
- 切记,不要每个地方都是用
-
禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用
%+v
进行统一打印,例如 http/rpc 服务的中间件。- 还可以记录原始错误的类型
log.Errorf("original error type:%T, error detail: %+v", errors.Cause(err), err)
- 还可以记录原始错误的类型
-
如果是一个包级别的err,error.New(“xxx”),xxx应该以包名开头,记录日志更清晰。
-
错误判断使用
errors.Is
进行比较。- 判断error类型时,不应直接使用
==
,1、err如果是指针,就不会想等。2、err如果包裹过errors.Wrap()
,也会不相等。应该使用errors.As
- 判断error类型时,不应直接使用
-
错误类型判断,使用
errors.As
进行赋值。 -
对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档。
重视业务错误
-
date层错误往上抛时,尽量转换成自定义的错误,不然上层如果要对这个error检查时,就会和db实现产生依赖关系。
errors.Wrap(RECORD_NOT_FOUND,"xxxx") errors.Withstack(RECORD_NOT_FOUND)
panic or error?
- 在 Go 中 panic 会导致程序直接退出,是一个致命的错误,如果使用panic recover 进行处理的话,会存在很多问题
- 性能问题,频繁 panic recover 性能不好
- 容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
- 不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理
- 什么时候使用 panic 呢?
- 对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用 panic
- 使用 error 处理有哪些好处?
- 简单。
- 考虑失败,而不是成功(Plan for failure, not success)。
- 没有隐藏的控制流(例如java的全局异常处理)。
- 完全交给你来控制 error。
- Error are values(这篇文章启发挺大,之前一直没明白go这样处理error的目的)。