你还在用官方的error库么,那就弱爆了

2020年6月2日 392点热度 0人点赞 0条评论
点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言
在一个项目中,你是否还在为出现error在哪里,定位error而苦恼?你是否还在为error中的信息太少而苦恼;那么我告诉你,本文会介绍给你一个非常好用的error库,让你追查error起来不再痛苦。

写在前面

在一个项目中,你是否还在为出现error在哪里,定位error而苦恼?你是否还在为error中的信息太少而苦恼;那么我告诉你,本文会介绍给你一个非常好用的error库,让你追查error起来不再痛苦。

我们今天的结构分为四个部分:

1 介绍
2 演示
3 源码解说
4 总结

本篇文章视频版地址:https://www.bilibili.com/video/BV1hE411c7Ze/ 需要复制链接打开bilibili观看。

图片

介绍

其实我们可以思考一下,我们在一个项目中使用错误机制,最核心的几个需求是什么?1 附加信息:我们希望错误出现的时候能附带一些描述性的错误信息,甚至于这些信息是可以嵌套的。2 附加堆栈:我们希望错误不仅仅打印出错误信息,也能打印出这个错误的堆栈信息,让我们可以知道错误的信息。

在Go的语言演进过程中,error传递的信息太少一直是被诟病的一点。这个情况在GO1.13之后有了些许改变,我们今天要推荐的库是:

github.com/pkg/errors.

这个库是由Dave Cheney开发的。他是VMWare的一名开发工程师,也是go语言的开源贡献者。

图片

这个是他的个人网站:https://dave.cheney.net/

演示

假设我们有一个项目叫errdemo,他有sub1,sub2两个子包。sub1和sub2两个包都有Diff和IoDiff两个函数。

图片

  1. // sub2.go


  2. package sub2


  3. import (

  4. "errors"

  5. "io/ioutil"

  6. )


  7. func Diff(foo int, bar int) error {

  8. return errors.New("sub2 diff error")

  9. }


  10. func IoDiff(foo int, bar int) ([]byte, error) {

  11. b, err := ioutil.ReadFile("filename")

  12. return b, err

  13. }


  14. // sub1.go


  15. package sub1


  16. import (

  17. "errdemo/sub1/sub2"

  18. "fmt"


  19. "errors"

  20. )


  21. func Diff(foo int, bar int) error {

  22. if foo < 0 {

  23. return errors.New("diff error")

  24. }

  25. if err := sub2.Diff(foo, bar); err != nil {

  26. return fmt.Errorf("sub1 error")

  27. }

  28. return nil

  29. }


  30. func IoDiff(foo, bar int) error {

  31. _, err := sub2.IoDiff(foo, bar)

  32. return err

  33. }


  34. // main.go


  35. package main


  36. import (

  37. "errdemo/sub1"

  38. "fmt"

  39. )


  40. func main() {

  41. err := sub1.Diff(1, 2)

  42. fmt.Println(err)

  43. }

我们可以看到,这里的sub1.go里面

  1. if err := sub2.Diff(foo, bar); err != nil {

  2. return fmt.Errorf("sub1 error")

  3. }

这种是最差的写法,它在返回sub1 error的同时,把sub2返回的错误信息完全掩盖掉了。

那么我们就遇到了我们两个需求点里面的第一个需求点,应该尽可能多的返回错误信息。

于是我们或许就会这么修改:

  1. if err := sub2.Diff(foo, bar); err != nil {

  2. return errors.New("sub1 error:" + err.Error())

  3. }

或许你也会想到用fmt的errorf来进行修改

  1. if err := sub2.Diff(foo, bar); err != nil {

  2. return fmt.Errorf("sub1 error: %s", err.Error())

  3. }

图片

显然第二种比第一种好很多,但是第二种方法有个最大的问题,它丢失了sub2的error的类型信息。

同样的写法我们放到IoDiff中我能感受出来。

  1. func IoDiff(foo int, bar int) ([]byte, error) {

  2. b, err := ioutil.ReadFile("filename")

  3. return b, err

  4. }

我们在main中想根据这个错误类型进行判断,如果是文件不存在的错误,我希望做一些诸如文件路径修改的操作。现在的情况是做不到的。

  1. func main() {

  2. err := sub1.IoDiff(1, 2)

  3. if err == os.PathError {

  4. ...

  5. }

  6. fmt.Println(err)

  7. }

于是在GO1.13中也考虑到了这个事情,引入了error的wrap机制。简要来说,就是我们上面的例子中,sub1我们就可以修改为这样:

  1. func IoDiff(foo, bar int) error {

  2. _, err := sub2.IoDiff(foo, bar)

  3. return fmt.Errorf("sub1 error: %w", err)

  4. }

这样我们在main中可以进行这样判断:

  1. func main() {

  2. err := sub1.IoDiff(1, 2)

  3. var perr *os.PathError

  4. if errors.As(err, &perr) {

  5. fmt.Println("do some fix")

  6. return

  7. }

  8. fmt.Println(err)

  9. }

图片

但是不管error的消息怎么封装,如果我们想打印堆栈信息,还是无法顺利打印出来。

所以我们今天介绍的包就派上用场了:https://github.com/pkg/errors

它的使用也是非常简单,只需要将官方import error包的地方直接替换成它就可以了。完全无缝对接。

替换完成之后,我们发现,还是一样的,可以运行,而且和替换之前的形态没有什么不同。

但是现在我们使用errors.New出来的包,是一个github.com/pkg/errors 库中的一个foundation实例了。

它的format方法可以使用fmt.Printf("%+v", err) 的方式给打印出来。

  1. // sub2.go

  2. func Diff(foo int, bar int) error {

  3. return errors.New("sub2 diff error")

  4. }



  5. // sub1.go

  6. func Diff(foo int, bar int) error {

  7. if foo < 0 {

  8. return errors.New("diff error")

  9. }

  10. if err := sub2.Diff(foo, bar); err != nil {

  11. return errors.Wrap(err, "sub1 error:")


  12. }

  13. return nil

  14. }


  15. // main.go

  16. func main() {

  17. err := sub1.Diff(1, 2)

  18. fmt.Printf("%+v", err)

  19. }

我们可以看下这个例子,在sub2中我们使用New来创建了一个带有堆栈和错误信息的error。然后在sub1中,我们使用了Wrap封装了一个带有堆栈和错误信息的error,同时也带着sub2的堆栈和错误信息给了main。

图片

我们研究下这里的Wrap,除了这个Wrap之外,其实我们还可以使用:

  • WithMessage

  • WithStack

这两个函数和Wrap的区别是在fmt.Printf的时候,是否带了错误信息,和堆栈。WithMessage是只带了错误信息,WithStack是只带了堆栈。

图片

这几个方法都可以看实际的情况来决定。

原理

其实 github.com/pkg/errors 的原理也是非常简单,它利用了fmt包的一个特性:

在 https://golang.org/pkg/fmt/ 里面有具体的说明fmt的打印顺序:

图片

其中在打印error之前会判断当前打印的对象是否实现了Formatter接口

图片

formatter这个接口的两个参数其实就是表示了我们打印的时候需要的“占位符”,“宽度”,“精度” 和“标记”。

比如我们使用fmt.Printf的时候,format中的 "%+3.2f" 中的 + 为标记,3为宽度,2为精度,%f为占位符

errors里面的错误类就实现了这个Formatter接口。

但是其实github.com/pkg/errors里面的错误类是实现了三种

图片

  • fundamental (标准,同时带有message和stack)

  • withStack (只带有stack)

  • withMessage (只带有message)

而只要带有message的错误类型,又都同时实现了Error()方法,也就是实现了errors包的error接口,可以完全当作error来使用。

总结

从这个包里面,我们就可以看到go的鸭子类型接口的优势了。只需要我们在函数调用的时候使用的是接口,那么就能很方便的进行扩展。

比如这里的github.com/pkg/errors包中的三种错误类型

他们一方面实现了fmt的Formatter接口,另一方面只要有错误消息的接口,又实现了内置的error接口

  1. // fmt.print.go

  2. type Formatter interface {

  3. Format(f State, c rune)

  4. }

  1. // buildin.go

  2. type error interface {

  3. Error() string

  4. }

甚至于在1.13出现了之后,这个库还实现了Unwrap

  1. // errors/wrap.go

  2. interface {

  3. Unwrap() error

  4. }

方法,也是支持wrap和unwrap的。

我们从go2 的draft(https://go.googlesource.com/proposal/+/master/design/go2draft.md) 中可以看到,在go2中的标准error也会支持使用 %+v 来打印堆栈了。到时候这个库应该就没有使用的必要了。作者在readme文档中也说了,1.0是最后的release版本,后续不再更新了。不过这个不妨碍它现在是最好用的error处理库。

欢迎关注轩脉刃的公众号:

图片

推荐阅读

8800你还在用官方的error库么,那就弱爆了

root

这个人很懒,什么都没留下

文章评论