1.首先何谓error
GO中的error就是一个普通的接口(实现了Error方法)
位于源码builtin.go中
type error interface {
Error() string
}
利用实践验证demo1
//该demo通过基础类型衍生出自定义类型,并类型断言成功
type MyString string
func (s MyString) Error() string {
return string(s)
}
func main(){
mystring:=MyString("look me")
if _,ok := interface{}(mystring).(error);ok {
fmt.Println("is an error")
}
}
2.如何创建一个error?
2.1.方式一errors.New
//errors.go
package errors
func New(text string) error {
return &errorString{text}//返回的是指针
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
通过阅读errors源码包我们不难发现,errors.New返回的是一个指针,并且创建错误的具体信息被保存在结构体中的字段中
2.1.1为什么这里返回的是一个指针,而不是一个结构体对象
我们首先需要明确一个事情,结构体类型之间做两两比较的时候即==,判断的是结构体中的所有成员是否相同(如下demo2)
type myTest struct {
s string
}
var a = myTest{"hi"}
func main(){
if a== (myTest{"hi"}) {
fmt.Println("true")//相同
}
}
也就是说,如果errors.New如果返回的不是指针,那么当我们自定义的错误类型就很容易和别人的产生"碰撞"
想想,我们定义了一个超时错误,同事也定义了一个超时错误,还破天荒的都用相同的结构去描述这个错误,那么结果就是当项目抛出一个超时错误时,两个竟然都可以用==匹配上,这合理吗?
指针保证了地址(错误)的唯一性,基础库中也大量使用了自定义error
//bufio.go
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)
2.2方式二fmt.Errorf
基础用法demo3
func main(){
mystring:=fmt.Errorf("look me")
fmt.Printf( "%T:%v\n" ,mystring,mystring )
mystring2:=fmt.Errorf( "%w look me again",mystring)
fmt.Printf( "%T:%v\n" ,mystring2,mystring2 )
fmt.Println( mystring==mystring2 )
}
//output
*errors.errorString:look me
*fmt.wrapError:look me look me again
false
fmt.Errorf除了可以抛出一个异常,还可以"封装"一个异常
关于wrapError源码位于fmt/errors.go
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
我们可以看到实现了error接口,也可以通过Unwrap方法不断追朔源头(相当于一个单向链表结构)
2.2.1关于go1.14errors包中新增的方法Is和As
errors.Is
当异常被多次封装时,我们上游可以通过Is方法来判断该异常是否是底层的某个异常(实际是递归调用Uwrap,修改demo3)
func main(){
mystring:=fmt.Errorf("look me")
mystring2:=fmt.Errorf( "%w look me again",mystring)
mystring3:=fmt.Errorf( "%w look me again",mystring2)
fmt.Println( errors.Is(mystring3,mystring2) )
fmt.Println( errors.Is(mystring3,mystring) )
}
源码位于errors/wrap.go
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 { //这里递归调用Unwrap
return false
}
}
}
errors.As
As所作的就是遍历error链,从里面找到符合类型的error,然后把这个error赋给目标
demo4
type ErrorString struct {
s string
}
func (e *ErrorString) Error() string {
return e.s
}
func main(){
var targetErr *ErrorString
err := fmt.Errorf("new error:[%w]", &ErrorString{s:"target err"})
fmt.Println(errors.As(err, &targetErr))
}
源码
func As(err error, target interface{}) bool {
//一些判断,保证target,这里是不能为nil
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
//这里确保target必须是一个非nil指针
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
//这里确保target是一个接口或者实现了error接口
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
//关键部分,反射判断是否可被赋予,如果可以就赋值并且返回true
//本质上,就是类型断言,这是反射的写法
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
//这里意味着你可以自定义error的As方法,实现自己的类型断言代码
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
//这里是遍历error链的关键,不停的Unwrap,一层层的获取err
err = Unwrap(err)
}
return false
}
3.关于panic
error的抛出不一定影响我们程序的正常退出,但是panic意味着程序的崩溃
如demo5,即使是协程引发panic,也会造成整个进程的异常退出
func main(){
defer func(){ //即使是主进程做recover也无法恢复协程引发的panic
if err:=recover();err!=nil{
fmt.Println("world")
}
}()
go func(){
fmt.Println("hello")
panic("bye")
}()
time.Sleep(3*time.Second) //后续不执行
fmt.Println("world")
}
为防止上面这种“野协程”导致整个进程挂掉的情况,我们通常都会有recover进行处理
func main(){
Go( func(){
fmt.Println("hello")
panic("bye")
} )
time.Sleep(3*time.Second)
fmt.Println("world")
}
func Go(x func() ){
go func(){
defer func(){ //通过在协程中recover对进程做保护
if err := recover();err !=nil {
fmt.Println(err)
}
}()
x()
}()
}
因此对于那些表示不可恢复的程序错误如索引越界,栈溢出,强依赖资源错误(缺少必须的配置文件导致后续任务无法执行),我们才使用panic,对于其他错误情况,我们期望使用error进行判断
4.优先失败
即函数的返回值若存在error,需优先考虑失败而不是成功,优先处理这个错误
5.错误类型
5.1预定义错误
即使用errors.New预先构造包级别的error
这个在上文介绍过
var (
TimeOut = errors.New("timeout")
...
)
缺点明显,不够灵活,调用方想知道具体信息必须使用error.Error()方法查看,还需通过==等值判断,若异常在传递过程中被包装(携带有意义的上下文信息如fmt.Errorf方法包装 )则无法使用==判断,无法查看堆栈信息(具体是哪个文件哪个函数哪一行抛出)
并且加大了包与包之间的耦合性
5.2自定义类型
我们既然知道了error的接口如何实现,不妨自定义一个error类型来告诉调用方抛出异常时的上下文环境
type MyError struct{
MSG string
File string
Line int
}
func (e *MyError) Error() string { //实现接口
return ...
}
但是缺点是当抛出一个异常时,我们需要做断言,如果为自定义异常则打印上下文信息
func main(){
err :=test()
switch err:=err.(type){
case nil:
断言成功 什么都不做,即没有错误
case *MyError ....
}
}
5.3非透明错误
即调用方只判断成功或失败
func fn(){
err :=Foo()
if err!=nil {
return err
}
}
但是我们无法看到内部具体什么错误
5.4断言错误
是对自定义类型的扩展,用接口来约束具体的错误类型
type template interface {
error
Timeout() bool //是一个超市错误吗
}
type MyError struct {
t bool
s string
}
func (e *MyError) Error() string {
return e.s
}
func (e *MyError) Timeout() bool {
return e.t
}
func IsTimeout(err error) bool {
te,ok := err.(template)
return ok && te.Timeout()
}
func main(){
err := &MyError{true,"timeout!!!"}
fmt.Println( IsTimeout( err ) )
}
可以看到template扩展了error接口,通过isTimeout函数来断言异常,如果断言成功则返回是否为超时错误
5.5Wrap Error
原本的error他抛出时几乎不懈怠抛出时的上下文信息,我们通过5.2的自定义错误类型来完善了他,那么当我们中间的调用者(并不是最后)捕获到这个异常时,想要再添加一些上下文信息时,这种方式显然是不够的,我们需要使用fmt.Errorf配合%w来包装error,但是这个包装后的error并不是原本的error需要通过Unwrap方法来追溯源头,这里的追溯源头就是wrap Error的思想,除去fmt.Errorf还有一个更好的wrap error方式就是使用第三方库(github.com/pkg/errors)
github.com/pkg/errors主要对外提供两种方法
Wrap和Cause
源码
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
func (w *withStack) Cause() error { return w.error }
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
我们可以看到这实际也是一个链表,链表的尾部是原始error,通过Wrap方法用链表节点的withMessage和withStack一层层的包装向外传递
解包使用Cause来获取原始错误
更多使用方法参考pkg/errors文档
简单demo6,利用pkg/errors携带堆栈信息
import (
"fmt"
"os"
xerror "github.com/pkg/errors"
)
func C() error {
_, err := os.Open("abc")
if err != nil {
err = xerror.WithStack(err)
return err
}
return nil
}
func main(){
err:=C()
if err!=nil{
fmt.Printf("stack error :%+v",err) //注意这里用%+v确保完整打印
}
}
推荐使用方式:
和其他库,比如标准库,github第三方库,自己的基础库,进行交互协作时使用Wrap,中间传递直接返回错误,而不是每个错误产生的地方到处打日志,在程序的顶部使用%+v打印堆栈
6.文章参考
1.Golang eror的突围
2.pkg/errors
3.golang中内嵌interface
4.GO中的日常错误对象
5.Error Wrapping深度分析
6.Go语言(golang)的错误(error)处理的推荐方案