我们上面介绍了一下Error Type它的基础定义
还记得我们那段话
Errors are value
它就是一个普普通通的interface
我们就要结合它的这个特性
我们看看Go的error到底有一些什么样的Type
它有一些什么类型的玩法
我们来看一下
我们第一种叫Sentinel Error
哨兵
把所有预定义的、特定的错误
我们都叫它为Sentinel Error
对于Go来说
我们使用特定的值来表示错误
在标准库里面大家如果认真去翻代码的话
应该是非常多的
类似像底层的io.EOF
更底层的就是syscall
我们系统调用的一些系统错误也是非常多的
基本都是预定义的
我们一般把预定义的、特定的某种错误
我们叫Sentinel Error
它使用的姿势是什么样的呢
基本上都是if err==某个对象
还记得我们之前提的吧
我们之前提的是必须要返回它特定的指针
这样的话你if err==某个Sentinel Error的时候
你就会做一个正确的判定而不会误撞
这个是Sentinel Error
使用sentinel值是一种不灵活的错误处理策略
它虽然有标准库也好
或者我们经常自己写基础库也好
写框架
我们经常会使用
但是它其实不是一种灵活的策略
因为调用方必须使用==将结果预先声明的值
来进行一个判定
你要用一个返回的值
导入这个包
然后用预定义的值
这个error去进行判定
当你想要获取更多的上下文
比方说从哪个地方报的错
为什么报错
是谁报错
如果你使用类似像fmt就是我们的format
我们标准库的fmt.Errorf去把它包了一层之后
你带上一些详细的错误信息
比方说创建一个文件路径报错
而这个路径的地址具体是什么
你把这个信息带回去以后
它这个错误就被破坏了
一旦破坏你的==就失效了
因为你用类似像fmt.Errorf携带更多详细的信息
它就会破坏调用者的==
就是你预定义的错误
通过fmt.Errorf包了以后
那就肯定代码会出错的
上层肯定判定失败的
这个问题其实也困扰我很长一段时间
刚开始用的时候不太习惯
我现在又想带一些详细信息
然后这种预定义的error
又无法携带更多详细信息
它只能逼着我当时调它的Error方法
把这个方法输出内容去看是否匹配特定的字符串
我不知道你们有没有做过这种方式
看这个Error返回的字符串是什么
然后去做一些这种模式的匹配或者字符串的匹配
看是不是这种类型的错误
因为你之前可能是通过fmt.Errorf
去包装了一些别的更详细的上下文
这个其实很尴尬
这个其实是非常不好的
所以这种做法在代码层面是一种很弱的约定
或者一种契约
很容易被破坏
所以这个场景里面
我是建议完全不依赖error.Error方法的输出
这个是一定要注意的
因为它太脆弱了你很容易出一些bug
不应该依赖检测error.Error方法的输出
因为Error方法存在于error接口
主要是用于方便程序员使用
比方做调试
但是这个其实是给程序员用的
并不是给程序用的
这是有本质区别
就给程序员做调试
写单元测试
但不是给调用者来使用的
这个输出的字符串用于记录日志
输出到终端
输出到StdOut等等
但是你不能用来在程序里面去做一些等值的匹配
寻找特定的字符串
非常得不安全
一定要注意这个点
重要的事情说三遍
不依赖检测error.Error方法的输出
这个一定要注意就是不能依赖它的输出
使用Sentinel Error还会有个毛病
Sentinel Errors它最终会成为你API的公共部分
这个什么意思呢
如果你的公共函数或者方法
返回了一个特定值的错误
就是返回了Sentinel Errors
那么该值
这个error你必须把它公开
因为别人要用
假定你的代码逻辑里面依赖
这种类似io.EOF、ErrBufferFull
bufio里面有个ErrBufferFull
这些肯定是预定义的
你肯定要加文档
我们所有公开的
注意大家有装一些Go link的工具
你去Go build的时候
它一定会检测你这种公开的方法
或者是全局的变量有没有文档记录
这种无疑会增加你API暴露的表面积
最终库对外暴露的方法
或者是包级别的变量肯定会变多
因为你的方法依赖这么一种特定的输出
特定的一个error
它肯定是要加文档、加定义
如果API定义的一个
注意如果你的API
定义了一个返回特定错误的interface
那么这个interface里面有个方法
一定会返回类似io.EOF这种错误
该接口的所有实现都将被限制为
一定要使用这个错误
就是你的interface
如果在interface描述里面写得清清楚楚
一定会返回io.EOF
作为一个哨兵的Sentinel Errors
那么肯定你的接口实现者也是要使用它的
比如io.Reader
类似io.Copy
最终都是要依赖io.EOF来告诉调用者
没有更多数据
而且这还不是个错误
你肯定是加文档
然后把io.EOF对外暴露
它无疑都会增加表面积
为什么要单独提它增加API的公共部分
我们在做基础库设计的时候
一定是对外暴露更少的方法
我们的包使用更简单
一定注意这句话
Rob Pike其实曾经也说过
你的接口表面积越大
你的抽象就越脆弱
一定要注意
下一个点是什么
Sentinel errors在两个包之间创建了依赖
Sentinel errors它最糟糕的问题
其实是在两个包之间创建了源代码层面的依赖关系
也就是说你使用别人提供的方法时候
因为你知道它会返回类似io.EOF这种error值
你的包肯定要导入io包
这个特定的例子听起来并不那么糟糕
因为很常见
我们基本上都会导入io包
但是想象一下如果你项目中有很多这种情况存在
很多这种导出的错误值存在
各种地方耦合
其实也间接会导致你的整个基础库的设计
很容易形成import loop
就是所谓的A导B、B导A这种情况
它的概率会被加大
因为你最终是创建了包级别的依赖
源码级的依赖
所以结论是什么
尽可能避免使用Sentinel errors
它的的确确在标准库
或者是在我们日常工作中经常会遇到
但是我们仍然建议尽可能避免使用它
它不应该是一个被经常模仿的模式
这种我们不建议
怎么办
有没有更好的方式
我们一个个看
下一种叫Error types
Error type实际上实现了Error接口的自定义类型
我有一个叫MyError的自定义类型
我们看一下
MyError有个自定义的类型
它加上了发生了什么事情
Msg有详细的记录
有行号有文件
哪个文件出现了什么问题
我们只要实现了Error方法
更详细的一些上下文的信息就搞定了
我抛出这个错误的时候
只需要返回MyError的一个对象就好了
我们在标准库里面也见到大量类似的做法
那么这样使用以后
比方说我调test方法
我们看test方法
它会返回这个错误
我们在main函数里面通过断言
因为它是个自定义的类型
你返回的是Error的interface
我这个时候用它的时候就要使用
一个switch type断言
switch加type断言
我先看它是不是nil
如果是nil表示成功
如果它断言成功
能断言成MyError这个对象
我就会把它打出来
因为它内部的一些字段
我都能获取到的
比方说line、file
Msg这些详细的信息我都能获取到
我觉得这个也是一个比较常见的用法
与错误值
与错误Sentinel Error相比的话
它的错误类型一大改进
就是能够包装更多底层的错误信息
提供给我们的调用方
有一个不错的例子是什么
是标准库的os.PathError
它提供了底层执行的什么操作
哪个路径出了什么问题
比方说它有Op(operation)
Path、Err
这个是标准库也是os.Path
就是os包里面的
你如果通过
你去使用的时候
你可以通过switch断言
能够获取它的详细类型
这个其实也是一个很常见的例子
所以我们再回过头来想一想
我们在自己使用写业务代码的时候
一定要多去参考开源的一些实现
标准库的一些实现
其实都会给到一些demo
但是我们在看别人提供的这些demo时候
你还是要有自己的一些思考
哪一种是好的、合适的
我们继续
调用者使用Error type
要使用类型断言和类型switch
这种方式其实也会让自定义的error变成public
因为你要断言成具体的对象
它自然而然
你自定义的MyError它一定是大写的
其实一样的
这种模型会导致和调用者产生源码级的强耦合
从而导致API变得脆弱
我们结论是一样的
尽量避免使用Error types
因为虽然错误类型Error types
它比sentinel errors更好
它可以获取更多的上下文
但是error types共享了
所有error values许多相同的问题
共享这种sentinel errors相同的问题
因此我的建议说也是尽量少用error types
或者至少避免将它变成你公共API的一部分
这是第二种
这种也不是很建议
那怎么办有没有更好的
我们再看一下第三种
我们下一种是什么
叫Opaque errors
不透明的错误处理
在我看来这是最好的
也是我们最推荐的、最佳的一种错误处理方式
它是最灵活的
因为它要求代码和调用者之间的耦合是最少的
相当于你返回了一个不透明的错误给到使用者
使用者不会管它具体是什么
只能判断error!=nil
是不是没有任何源码级的依赖
我们把这种风格称为不透明的错误处理
因为你虽然知道发生了错误
但是你没有能力看到错误的内部
你不知道它内部是什么东西
那么对于调用者来说
我们大部分情况其实只关注操作结果
你所知道的它起作用了
或者没起作用
是成功还是失败就可以了
这是不透明错误的全部功能
只需返回错误
而不假设其内容
没有任何东西
我觉得这是最好的
但是Opaque errors
其实这种方式的话
你拿不到上下文怎么办
越来越有意思了
Opaque errors
没有给你暴露行为出来
我们看看有个小技巧
我们看一下这段话
叫assert errors for behavior not type
我们去获取 error
你需要获取它一些内部的详细的信息时候
我们可能会使用类似自定义的MyError这种做法
我们前面不是有一个Error type
MyError自定义了一个
对外暴露的是MyError这个类型
但是我们实际上要的是什么
我们要的是动作
我需要拿到的是它的Msg、文件名、行号
如果我换一种思路
我对外暴露的是它的行为
我能获取到它的行为
但是我不把类型暴露出来能不能做到
我们再看一下
一些情况下如果你不需要获取详情的情况下
我觉得是OK的
你就什么都不要做了
但是在一些少数情况下
这种二分错误的处理方式是不够的
例如我们与进程外的世界进行交互
比方说网络
你需要调用方调查错误的性质
以确定要不要重试
比方说我们类似epoll
它可能会返回类似异步的block之类
你可能要做重试
在这种情况下
我们可以断言错误
断言它是否实现了特定的行为
而不是说断言出它特定的类型
我们再看个例子
比方说在内部定义一个小写的interface
注意这个地方是小写的interface
它没有对外暴露
它只是说在包级别可见
然后它有个叫Temporary的方法
我这个时候提供了一个IsTemprorary这样的行数
然后传参是error
这个时候我就可以把error做一个断言
在包内部做一个断言
这个时候其实没有对外暴露任何自定义错误类型
而是通过一个内置的interface
然后我在内部断言
然后再返回IsTemperary这个方法
它返回的是一种行为
我们再看看这种姿势
其实在标准库也有
net包里面
net包里面有一个它自定义的Error
它是一个interface
我们看一下
它有一个方法叫Timeout
有个Temporary
真正在用的时候其实会这样
先用error把它断言成net.Error
然后再获取它内部的行为
这样的做法其实也是把它的行为给暴露出来
但是它的做法不好的地方是
net.Error这个interface它是对外暴露的
但是如果我们换一种思路
我们只是在net包加了一个IsTimeout
IsTemporary
是不是连这个类型也不暴露
只是提供了两个方法
当然这是两种取舍
这个完全看你
我觉得某种意义上都是让你少暴露类型
让你少暴露类型
因为强依赖某一种类型的话
它的扩展性肯定是不好的
所以说我们看net包
它实际上定义的是一个interface
这里的关键是什么呢
这个逻辑就是不导入自定义错误时候的包
或者实际上不了解error底层的类型情况下
对error底层是不了解的情况下
通过行为就能获取到一些信息
我觉得是比较好的一种姿势
Opaque就是先不返回任何详细信息
只让你判断错误
是成功还是失败
第二种方式告诉你
你可以通过断言它的行为来获取一些更详细的信息
所以其实这也是我们最鼓励的一种处理的方法
是否就完结了
没有
为什么
因为这么一种姿势你还是会觉得非常麻烦
这也是我们后面再讲Wrap error的时候
错误包装的时候
我们会再展开讲
把这几种情况再结合能不能做得更好