同系列文章:Go 进阶训练营
和java相比,go的异常处理两极化,panic比exception更严重,java exception是线程级别的,而go的panic是进程级别,任意goroutine出现panic都会导致整个进程挂掉,更能提醒异常情况。error比exception更轻微,在go中,error是当做值来处理的,更加灵活、细致,但需要大量的if err!=nil
(考验代码功底的时候到了)。而exception的全局异常捕获用起来更方便、笼统。整体来讲,各有利弊(废话,要是绝对碾压就不会都存在了)。
在程序启动的时候,如果有强依赖的服务出现故障时 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()
}()
}
我们在应用程序中使用 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 服务的中间件。
错误判断使用 errors.Is
进行比较。
==
,1、err如果是指针,就不会想等。2、err如果包裹过errors.Wrap()
,也会不相等。应该使用errors.As
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是一个方法。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
}
runtime.strequal
《go语言精进之路》里string章节详细讲了string比较,后面会分享笔记。
type..eq.main.MyError
type MyError struct {
a int
msg string
}
结构体变量比较:结构体字段逐个比较。推测这里的的equal表示这个意思。
==
比较,可以使用反射包里的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进行包裹。
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,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档。
重视业务错误