我们接下来要讲Go1.13的一些特性
我们上一章主要是讲了一下
如何去消除if err!=nil
我们知道要写很多这样的判定
然后也讲了最核心的像Wrap error
去把整个错误的一些堆栈信息进行保存
也讲它的一些使用的规则
我们接下来要看一下1.13的一些新特性
如何跟我们的Wrap error去进行一个结合
在1.13之前
我们最简单的错误检测就是if err!=nil
大家也比较熟悉的
我们有时候需要对一些预定义的错误
类似像我们这个地方ErrNotFound
我自己预定的一个NotFound的一个错误
然后我在使用的时候
就需要进行if err==NotFound这种方式来使用
我们在Error Types里面专门讲了
Sentinel Error的一个检测的一个方式
同时一旦实现了error interface的自定义
一个结构体的话
就是用断言的方式来获取更丰富的上下文
比方说在这个地方
我们可以自己定义一个NotFound error
然后加上一些字段信息
然后只要你实现了Error方法的话
我就很容易去扩展它
然后在使用的时候
对于这种errors type的这种方式的
我们只要断言成具体的详细的类型
我就能获取到它的详细的一个内容
假设我这个时候需要在函数的调用栈中
需要添加更多的一些上下文去向上传递
我们一般会使用什么
就是标准库的fmt
fmt.Errorf
一旦用这样的一种方式去Wrap了原始错误
那么你的原始错误的信息其实就丢失了
就是我们所谓的根因
我们怎么办
像这个地方我就定义了一个QueryError
那么QueryError这个结构体
它会包含Query字段
就是具体的查询哪个地方报错了对吧
然后也会包含一个Err字段
我需要知道它底层根因的错误是什么
最原始的错误是什么
程序在使用的时候可以去查看
QueryError的值是什么
我们其实只要拿到error进行断言
这个标准的error types的一个使用方式
我们在前面也介绍过
在1.13为errors标准库和format标准库
引入了新特性
来简化我们处理
类似像pkg/errors一样的
就是错误包装的一个这种方式
它怎么做的呢
只需要我们的error
实现了返回底层错误的一个叫Unwrap的方法
Unwrap相当于是把底层的根因的错误抛出来
这个场景里面比方说有个error1
我调了error1.Unwrap
返回的是error2 e2
我们就说e1包装e2
因为e1调了Unwrap之后返回e2
所以说它是e1包装了e2
然后你就可以展开e1获得e2
你通过e1实现了Unwrap的方法
获得底层的根因
按照上面的约定
我们把上面的QueryError
我们是不是也可以让它实现一个Unwrap的方法
把底层的错误给返回回来
所以Go1.13的errors
它在errors包里面
它增加了两个函数
一个叫Is一个叫As
我们大家听名字应该也知道
就是Is的话
有点类似于判定一个error错误
就是看它是不是等于你期望的一个target
比方说一个Sentinel Error
我们看这张图
if error==ErrNotFound这种方式
其实就等同于1.13可以这样写
If errors.Is(err)这个是一个变量
然后等于一个哨兵值ErrNotFound
为什么要这样的方式判定呢
因为errors.Is它的内部
它会不断地帮你调Unwrap去展开
寻找这个根因是否等于你匹配的目标
这样的话其实就类似于之前pkg/errors这个包
它有一个Cause的方法
Cause的方法内部其实跟Is的内部实现类似
就是不断地去循环
把它一层层剥开
找到它的根因
然后进行一个Sentinel Error的一个等值判定
同样的还有一个As方法
As方法的话实际上是
我们看一下这段代码
我们以前是需要断言
断言成QueryError
然后拿到一个是否OK断言成功
新的方式的话我们先var定义一个变量
定义一个QueryError的变量
然后我们再用errors
把你的错误转换成QueryError的一个值
你就能拿到它具体的QueryError的值
然后errors返回的是个布尔类型
就是它能否转化成对象
然后如果转换成功的话
这个变量这个QueryError定义的变量
就可以被使用
所以非常方便
所以我们最终
我们非常鼓励就是说
全部使用If errors.Is或者是As来使用
就是做等值判断
和做类型转换
通过Is和As它内部其实帮你做了Unwrap
Unwrap去帮你展开
所有的具体的一些根因的错误
我建议大家还是要认真看一下
1.13的新特性的一些文档和介绍
其实官方也是应该是纠结了很久才加入了这个特性
那么刚刚说了它还会扩展fmt包
那么fmt.Errorf呢
实际上是向错误添加附加信息
我们这个地方看一下
以前我们之前说过fmt.Errorf
它会导致原始错误的上下文信息丢失
现在的话它通过一个叫%w
新的一个位次叫%w
把这个error给包装起来
我们之前我们知道用pkg/errors的话
要调errors.Wrap去包装对吧
但是你使用fmt.Errorf的话
是1.13新提供的一个新特性
%w它相当于内部会帮你把对象给包装起来
那么它具体是怎么实现的
实际上它类似有这样的一个叫Wrap error的一个
一个私有的struct
我们看一下这段代码
error的话是返回它的message
然后Unwrap的话返回的是它底层的error
所以说如果你用fmt %w的话
实际上就是类似用这种方式
去把它进行了一个包装
那么用%w包装以后的错误
你就可以用errors.Is或者As
比方说我们这个地方把ErrPermission错误
就access denied的错误
用%w包装以后
你可以用errors.Is来做判定
也就是说这个error通过这样的一个方式
获得了更多的上下文
就可以获得更多的信息
你比方说这个地方fmt.Errorf
我带了很多扩展的信息
但是我又可以通过error.Is来做
Sentinel Error的等值判断
这样的话其实是非常方便的
我们就要出一个作业了
我们在数据库操作的过程中
也就是我们的DAO层
我们的DAO层遇到你的查询的一个SQL查不到数据
通常它会返回一个sql.ErrNoRows
那么这个时候请问
我们是否应该Wrap这个error
抛给上层
为什么
需要写个demo
到底遇到找不到的时候
没有 NoRows的时候
我们应该怎么去处理
让我们的业务逻辑层
去调我们的DAO层的代码的时候
应该怎么去处理
我们可以写一个这样的作业
尝试一下再思考一下
那么我们去看一下
1.13它的Is和As的实现方法其实也非常简单
我们看它这个代码核心是什么
就是这个for循环
如果这个error能断言
1.13它的Is方法也可以被扩展的
因为假设我们知道有一些这种
两个error判定是否相等
假设它是两个详细类型
你可以去做一些自定义的逻辑对吧
你看我们这边这个地方是Path和User两个字段
我们可以扩展实现这个Is方法
按照你自己想要的方式
就是未必是按两个对象完全相等
你可以判定它里面的字段是否相等
所以你可以自己去扩展这个Is方法
1.13的errors这个Is方法
实际上它是先会判定你是否实现了Is方法
假如没实现的话它就会不断地调Unwrap
去把你的根因的错误再去拿到
如果拿到根因错误以后
最终我们看到第一行代码
它会判定你这个error跟target对象
target对象就是目标对象是否相等
如果相等的话就返回true
所以你既可以自定义去实现这个Is的判定
同时也可以用实现Unwrap的方法
把根因的错误拿到
和Sentinel Errors进行等值判断
这样的话你可以扩展Is或者是As
或者是你只要单纯的实现它的Unwrap方法
你就会很方便去扩展
Go1.13的这个errorWrap这个功能了
我们再来看一下
这里还有个方法
标准的一个用法
我们先定义一个哨兵的错误
Sentinel Errors
有一个叫ErrPermission的一个变量
定义完之后我们有一个函数叫DoSomething
如果这个用户没有权限
我们以前的代码是怎么写的
If err:=pkg.DoSomething
然后err==pkg.ErrPermission
这是我们以前的做法
这样的做法大家也知道坏处了
你只能强依赖这个ErrPermission的定义
然后你无法去追加更多的上下文信息
也就是说你最终你的Debug成本
你的调试成本会变得非常高
通过1.13的方式的话我们看一下
你可以return一个fmt.Errorf %w
把ErrPermission给Wrap住
%w实际上在内部其实就是一个
刚刚说的自定义的wrapError的结构体
其实它返回的就是实现了Unwrap的
一个这样的一个结构体
你判定的时候还是一样的
我们这个地方就换成了errors.Is
把这个error对象
虽然通过fmt.Errorf携带了一些上下文
但是我可以通过errors.Is把它的根因的错误拿到
然后再去做一个等值的判断
这样的话就完成了我上面说的功能
但是目前来说Go1.13
它虽然实现了Wrap的功能
但是不具备获取堆栈信息
所以说我前段时间也去看了一下pkg/errors
pkg/errors目前是兼容了1.13的
errors的Is和As方法
那么pkg/errors它也实现了Unwrap的方法
所以它兼容了Go的1.13
所以目前来看我们还是得持续使用
pkg/errors的Wrap方法
有了pkg/errors的Wrap方法
它包含了堆栈信息
那1.13还没有包含
所以我建议大家是可以使用errors
1.13的Is和As的方法
但是你如果要包含堆栈信息
暂时先不要用
fmt的%w这个语义
还是先用pkg/errors的Wrap方法
可以包含堆栈信息
但是剩下的Is和As还是可以使用的
甚至你可以自己去扩张它的Is方法
整个pkg/errors对1.13都进行了一个兼容
这一块其实就是整个1.13的一些特性
我相信Go2.0它有更大的一些改变
包括像堆栈信息的保存等等
因为我们知道Go1的话它是要向下兼容的
所以它很多这种破坏性的变更都没有去做
那Go2的整个它的一些特性的话
我这边是列了三篇文章
也是他们当时设计的一些文档
主要是包含几块
一个就是说Wrap error这块的一个重大改进
还有一块是关于关于error handing的
我们知道在一个其实我们讲的一些
消除if err!=nil的一些方法
但是实际上现在
还是会写大量的一些冗余的这种代码
那么Go2的话提出了一个
叫handle和check两个关键字
大家感兴趣可以搜一下
Go2的叫check error
check error和handle arrow
那么它有一些新的关键字
但是它跟try catch又有点不太一样
Go2其实这块正在起草去做一些设计
我们也可以持续对这块进行一个关注
因为error的话
应该是大家对Go语言最关注的一个点
所以也是有很多人会来挑战Go
为什么要这种方式去处理
所以我觉得随着Go1的Wrap error的改进
或者是Go2的check handle的这种处理方式
或者是说在Go1.13的
Wrap error的增强加上堆栈信息
我觉得逐渐地让我们这个语言更加的成熟
然后你用起来也会更加方便
然后我们总结一下
前面就讲过关于error的一些基本的使用
我们谨记errors are value
它就是个普通的一个interface一个定义
第二块我们介绍一下error常用的一些姿势
像Sentinel Error、 Error Types
然后还有像我们的叫不透明的错误处理
包括我们也介绍了一些Handling Error
减少if err!=nil
或者是使用Wrap error
我们往上层去包含一些堆栈信息
并且我们应该讲过
我们应该只处理error一次
然后加上1.13的一些特性
和pkg/errors和1.13的特性的结合
会让我们整个代码
处理error变得更得心应手
我也建议大家在自己的项目里面去尝试一下
对你的项目工程进行一些改造
我相信收获会非常之大
然后我们如果在课后再去
了解一下Go2的一些发展
我觉得也是非常好
然后在整个References
我包含了大量的我找到的
就是关于error的一些使用的最佳实践
包括有官方的blog的
也有Dave(Dave Cheney)的文章
都建议大家去详细看一看
那么关于Go这一块的
error的这块东西我们就讲到这里
下一章节主要讲Go的并发
我相信这个也是非常重要的一个Go的特性