您现在的位置是:网站首页> 编程资料编程资料
一篇文章带你轻松搞懂Golang的error处理_Golang_
2023-05-26
414人已围观
简介 一篇文章带你轻松搞懂Golang的error处理_Golang_
Golang中的error
Golang中的 error 就是一个简单的接口类型。只要实现了这个接口,就可以将其视为一种 error
type error interface { Error() string } error的几种玩法
翻看Golang源码,能看到许多类似于下面的这两种error类型
哨兵错误
var EOF = errors.New("EOF") var ErrUnexpectedEOF = errors.New("unexpected EOF") var ErrNoProgress = errors.New("multiple Read calls return no data or error") 缺点:
1.让 error 具有二义性
error != nil不再意味着一定发生了错误
比如io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个错误
2.在两个包之间创建了依赖
如果你使用了io.EOF来检查是否read完所有的数据,那么代码里一定会导入io包
自定义错误类型
一个不错的例子是os.PathError,它的优点是可以附带更多的上下文信息
type PathError struct { Op string Path string Err error } Wrap error
到这里我们可以发现,Golang 的 error 非常简单,然而简单也意味着有时候是不够用的
Golang的error一直有两个问题:
1.error没有附带file:line信息(也就是没有堆栈信息)
比如这种error,鬼知道代码哪一行报了错,Debug时简直要命
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
Error 1406: Data too long for column 'content' at row 1
2.上层error想附带更多日志信息时,往往会使用fmt.Errorf(),fmt.Errorf()会创建一个新的error,底层的error类型就被“吞”掉了
var errNoRows = errors.New("no rows") // 模仿sql库返回一个errNoRows func sqlExec() error { return errNoRows } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return fmt.Errorf("sqlExec failed.Err:%v", err) } return nil } func TestErrWrap(t *testing.T) { // 使用fmt.Errorf创建了一个新的err,丢失了底层err err := serviceNoErrWrap() if err != errNoRows { log.Println("===== errType don't equal errNoRows =====") } } -------------------------------代码运行结果---------------------------------- === RUN TestErrWrap 2022/03/26 17:19:43 ===== errType don't equal errNoRows ===== 为了解决这个问题,我们可以使用github.com/pkg/error包,使用errors.withStack()方法将err保
存到withStack对象
// withStack结构体保存了error,形成了一条error链。同时*stack字段保存了堆栈信息。 type withStack struct { error *stack } 也可以使用errors.Wrap(err, "自定义文本"),额外附带一些自定义的文本信息
源码解读:先将err和message包进withMessage对象,再将withMessage对象和堆栈信息包进withStack对象
func Wrap(err error, message string) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: message, } return &withStack{ err, callers(), } } Golang1.13版本error的新特性
Golang1.13版本借鉴了github.com/pkg/error包,新增了如下函数,大大增强了 Golang 语言判断 error 类型的能力
errors.UnWrap()
// 与errors.Wrap()行为相反 // 获取err链中的底层err func Unwrap(err error) error { u, ok := err.(interface { Unwrap() error }) if !ok { return nil } return u.Unwrap() } errors.Is()
在1.13版本之前,我们可以用err == targetErr判断err类型errors.Is()是其增强版:error 链上的任一err == targetErr,即return true
// 实践:学习使用errors.Is() var errNoRows = errors.New("no rows") // 模仿sql库返回一个errNoRows func sqlExec() error { return errNoRows } func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) // 包装errNoRows } return nil } func TestErrIs(t *testing.T) { err := service() // errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true if errors.Is(err, errNoRows) { log.Println("===== errors.Is() succeeded =====") } //err经errors.WithStack包装,不能通过 == 判断err类型 if err == errNoRows { log.Println("err == errNoRows") } } -------------------------------代码运行结果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded ===== 例子解读:
因为使用errors.WithStack包装了sqlError,sqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用==无法判断出底层的sqlError
源码解读:
- 我们很容易想到其内部调用了
err = Unwrap(err)方法来获取error链中底层的error - 自定义error类型可以实现
Is接口来自定义error类型判断方法
func Is(err, target error) bool { if target == nil { return err == target } isComparable := reflectlite.TypeOf(target).Comparable() for { if isComparable && err == target { return true } // 支持自定义error类型判断 if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { return true } if err = Unwrap(err); err == nil { return false } } } 下面我们来看看如何自定义error类型判断:
自定义的errNoRows类型,必须实现Is接口,才能使用erros.Is()进行类型判断
type errNoRows struct { Desc string } func (e errNoRows) Unwrap() error { return e } func (e errNoRows) Error() string { return e.Desc } func (e errNoRows) Is(err error) bool { return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() } // 模仿sql库返回一个errNoRows func sqlExec() error { return &errNoRows{"Kaolengmian NB"} } func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) } return nil } func serviceNoErrWrap() error { err := sqlExec() if err != nil { return fmt.Errorf("sqlExec failed.Err:%v", err) } return nil } func TestErrIs(t *testing.T) { err := service() if errors.Is(err, errNoRows{}) { log.Println("===== errors.Is() succeeded =====") } } -------------------------------代码运行结果---------------------------------- === RUN TestErrIs 2022/03/25 18:35:00 ===== errors.Is() succeeded ===== errors.As()
在1.13版本之前,我们可以用if _,ok := err.(targetErr)判断err类型errors.As()是其增强版:error 链上的任一err与targetErr类型相同,即return true
// 通过例子学习使用errors.As() type sqlError struct { error } func (e *sqlError) IsNoRows() bool { t, ok := e.error.(ErrNoRows) return ok && t.IsNoRows() } type ErrNoRows interface { IsNoRows() bool } // 返回一个sqlError func sqlExec() error { return sqlError{} } // errors.WithStack包装sqlError func service() error { err := sqlExec() if err != nil { return errors.WithStack(err) } return nil } func TestErrAs(t *testing.T) { err := service() // 递归使用errors.UnWrap,只要Err链上有一种Err满足类型断言,即返回true sr := &sqlError{} if errors.As(err, sr) { log.Println("===== errors.As() succeeded =====") } // 经errors.WithStack包装后,不能通过类型断言将当前Err转换成底层Err if _, ok := err.(sqlError); ok { log.Println("===== type assert succeeded =====") } } ----------------------------------代码运行结果-------------------------------------------- === RUN TestErrAs 2022/03/25 18:09:02 ===== errors.As() succeeded ===== 例子解读:
因为使用errors.WithStack包装了sqlError,sqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用类型断言无法判断出底层的sqlError
error处理最佳实践
上面讲了如何定义error类型,如何比较error类型,现在我们谈谈如何在大型项目中做好error处理
优先处理error
当一个函数返回一个非空error时,应该优先处理error,忽略它的其他返回值
只处理error一次
- 在Golang中,对于每个err,我们应该只处理一次。
- 要么立即处理err(包括记日志等行为),return nil(把错误吞掉)。此时因为把错误做了降级,一定要小心处理函数返回值。
比如下面例子json.Marshal(conf)没有return err ,那么在使用buf时一定要小心空指针等错误
要么return err,在上层处理err
反例:
// 试想如果writeAll函数出错,会打印两遍日志 // 如果整个项目都这么做,最后会惊奇的发现我们在处处打日志,项目中存在大量没有价值的垃圾日志 // unable to write:io.EOF // could not write config:io.EOF type config struct {} func writeAll(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { log.Println("unable to write:", err) return err } return nil } func writeConfig(w io.Writer, conf *config) error { buf, err := json.Marshal(conf) if err != nil { log.Printf("could not marshal config:%v", err) } if err := writeAll(w, buf); err != nil { log.Println("count not write config: %v", err) return err } return nil } 不要反复包装error
我们应该包装error,但只包装一次
上层业务代码建议Wrap error,但是底层基础Kit库不建议
如果底层基础 Kit 库包装了一次,上层业务代码又包装了一次,就重复包装了 error,日志就会打重
比如我们常用的sql库会返回sql.ErrNoRows这种预定义错误,而不是给我们一个包装过的 error
提示:
本文由神整理自网络,如有侵权请联系本站删除!
本站声明:
1、本站所有资源均来源于互联网,不保证100%完整、不提供任何技术支持;
2、本站所发布的文章以及附件仅限用于学习和研究目的;不得将用于商业或者非法用途;否则由此产生的法律后果,本站概不负责!
