我们前面介绍了几种Error Type
包括像Sentinel Error
然后有自定义的错误类型
我们叫Error types
然后也有叫Opaque不透明的错误
还有我们通过Opaque errors
使用方法断言的方式
能够获取更多的上下文信息
这几种介绍完之后
关键是我们似乎没有彻底解决
我们想要类似获取更多上下文信息
并且又能判定它具体是什么Error
我们知道有可以通过行为判定
那是一种方式
我们觉得似乎还不那么好用
接下来我们要讲如何去处理这个错误
就Handling Error
就通过一些什么样的代码技巧
能够让整个过程变得更加的友好
我们在讲Handling Error的时候
它有很多小细节
其实都是大家平常写代码的时候
容易不注意的地方
我们先看一下这段代码叫
无错误的正常流程代码
将成为一条直线
而不是缩进代码
我们比较建议的方式是什么
比方说我们调了os.Open
打开了一个文件
那么我们一般会把错误的代码
通过缩进的方式在这里提前处理掉
我们叫handle error
然后do stuff就做一些事情
这个是我们经常建议的方式
我们不太建议说if err==nil
然后在这个里面去做正确的事情
然后在下面处理handle error
因为会导致你正确的代码全部会缩进
这代码写起来不好看
其实也是在我们code review的时候
要注意的一个地方
第二个是什么呢
我们再看看这个代码有什么问题
这个叫AuthenticateRequest
这个方法我们看一下
它调用了根据这个Request请求参数
来验证这个用户是否合法
我们调了这个判定
然后它返回了一个error
然后if err!=nil
我们把它return掉
也符合我们刚才说的规范
然后如果它正常的话
return nil
这种代码其实我在工作中经常见到
可能大家一不留神就会写成这样
实际上我们多写了很多代码
因为它直接返回的是error
我可以直接把它return回去就好了
其实都会让我们的if err!=nil变得少一些
其实都是自己写出来的
就没有那么多
这个其实很小的一个技巧告诉大家
我们再看一下这个代码
它是统计一个io.Reader
读取内容的行数
就是我现在需要
知道这个io.Reader对象里面有多少行
我们怎么做呢
我们拿到一个io.Reader对象之后
我们通常会建议使用bufio
我们bufio new一个reader
把这个r作为参数传入进去
拿到一个br对象
然后我会声明一个lines
一个lines的变量在统计有多少行
那么有可能在读io.Reader对象的时候
它可能会报错
我们那个函数签名里面
其实既返回行数也会返回error
这个时候我就会写个for循环
然后我们调ReadString
然后按照\n来进行切割
这个地方注意
这个地方有个骚操作是什么
我们使用lines++
为什么要这里先做行数的累加
再判断error
我们一般不是如果有error报错的话
我们一般是什么
立马会要去处理
那是因为如果有io.EOF的话
你跳过的话就会少记录一行
所以说这个地方有个小技巧
就是先对行数进行累加
然后再处理异常
因为有可能会返回io.EOF
那么你可能会漏掉一行
这个地方做了一个小调整
这个代码其实看起来很长
然后如果跳出的话
我们先判定一下
如果它不是io.EOF
说明它读的过程中报错了
我就要返回这个error
如果没报错我就返回lines
就是具体多少行
然后err为nil
我相信大部分人写成统计行数的话都会这样去写
但是这样的一个做法有什么不好呢
实际上写的代码比较多
我们再来看看精简版本改进版本
改进版本也是CountLines一样的
函数签名都一样
但是我这个时候
使用bufio的一个叫NewScanner扫描器
我们把reader传入到参数中
拿到一个sc对象
然后一样的我们用lines:=0
这个时候代码就非常简单了
我们只要判定
因为你扫描
不断地扫描
我只要调Scan方法
这个Scan方法它会返回true或false
如果还有下一行
那么它就会返回true
如果没有的话它就会返回false
那么我只需要在这个for循环里面
做一个行数的累加
然后在最后的时候我再return行数
因为我们在整个读的过程中
有可能会遇到io.EOF读完了
但是也有可能io.Reader报错了
中途可能就报错了
这个时候我需要跳出
所以它就会返回false
最后在判定error的时候
我需要调一下sc.Err这个方法
来获取Scan的内部
如果读io.Reader对象报错的时候
它会把error暂存
就临时保存起来
这样代码是不是精简了很多
这个代码我们再回想一下
大家在用database/sql的时候
是不是database
我们去查询一个MySQL的一些数据的时候
我们叫query
那会返回一个什么
sql.Rows这个对象
那么sql.Rows这个对象也是类似的
Rows.Nest这个方法
我们在这个代码里面可以调Scan
来获取具体的函数内容
但是这个地方注意
所有类似像这种扫描器的模式
在我们return的时候
一定注意Rows.Err{}
或者是这个Scanner.Err{}一定不能丢弃
否则就有可能会导致你的MySQL
读一半的时候报错
那么你这个error没有处理
就是说它实际上报错了但是
因为你这个地方没有返回Rows.Err{}
导致我们的代码会出现一些bug
这个一定要注意
这个地方的尾部要拿到Scanner的error
或者是Rows.Err{}
这个sql的Rows.Err{}
这个地方一定要注意
所以我们建议大家在code review的时候要注意
或者是我们内部如果有一些CICD的工具
这个地方一定要检测
这种代码的写法其实会让
对比前面这个版本是不是少写了很多代码
所以说老有人觉得
用err!=nil这种处理模型
会导致多写很多代码
但是我们试想一下这个Scanner内部的实现
实际上我认为它就是会把io.EOF的特殊处理
放到这个Scan方法用true或false来处理
以及如果读一半的过程中报错了
会把具体的error保存起来
通过提供一个error方法可以把它返回回去
其实就是暂存然后再返回
我觉得这样的一个模式其实
代码其实看起来就比较爽了
这都是几种小技巧
让我们的Error Handing少写代码
再看下一个例子
这个代码是net/http
就是标准库的net/http里面的
假设我们现在有一个Header对象
它是key value
就是HTTP状态码
HTTP的头还有状态码Code Reason
我们都知道HTTP的头和状态码
关键是什么
我们要WriteResponse
返回HTTP的response
返回它的请求
那肯定是要写到这个io.Writer对象
然后要写状态码
然后要写headers
然后要处理body
它会怎么写
首先就是第一步fmt.Fprintf
我们知道HTTP1.1的response
一开始都是HTTP/1.1
然后把它code和message给拼进去
这个地方因为你调了fmt.Fprintf
那么如果它有error的话
你这个时候就需要什么
就是要判定一下if err!=nil
如果报错的话肯定是return掉
关键是你下一步
你肯定是要把它的自定义的head头给写进去
那么你同样的要调fmt.Fprintf
把它的key value给迭代地去处理掉
这个时候也是一样的
如果中间有异常
你需要调return
最后的话我们还要把
我们还需要什么
我们还是不是还要加一个换行符
然后因为head结束之后有个换行符
又要处理一下if err!=nil
最后的话我们再调一次io.Copy
把body内容给拷进去
最终返回
整个WriteResponse
我们看一下处理了一次
两次
三次
三次if err!=nil
所以大家为什么会经常吐槽
你多写了多少代码
肯定不爽
有没有比较好的一个优化技巧
Opaque可在EPA那个文章里面
介绍的一个思路我们看一下
我们定义一个errWriter的对象
它包含io.Writer和一个error
这个error为什么
是不是有点像我们暂存存起来
临时储存起来的那个error
有点像我们刚刚说的Scanner和Rows都是类似的
我们再看这errWriter
它提供的一个Writer方法
然后把这个buf要刷进去
如果之前暂存的这个err!=nil
我就直接返回什么都没输出
然后把暂存的这个error给抛出去
否则的话就是正常的一个write的输出
就是这么一个通过errorWrite
这么一个包装的方法
我们看看它这代码怎么写的
WriteResponse它就变得非常简单
首先我拿到一个errorWriter对象
我把Writer放进去
拿到ew对象
然后我这个时候调fmt.Fprintf(ew)
不是这个w
ew它是errWriter的一个包装类
然后同样的HTTP/1.1
把Code和状态码放进去
注意这个地方没有做任何的错误处理
如果我的errWriter已经报错了
你不管传什么buf内容进来
我这边都会判断它之前已经报错了
我不做任何的处理
我不会污染已经写到buf里面的内容
而是说我不做任何处理
所以说这个地方我没有做任何判定
假设我到heads的这个for循环
迭代去写这个head头的时候
key value的时候一样的
如果在前一步
这个errWriter已经报错了
那么在我的输出这个head的时候
不需要做任何处理
因为如果它已经报错
我就已经跳过这个错误
再到空行\r\n
然后再到io.Copy
我们对比前面的代码是不是
整个if err!=nil一行都没有处理
我们实际上是在什么
是在errWriter里面处理掉的
大家都复用这行代码
所以其实整个代码就变得非常的简洁
你的主干逻辑
WriteResponse的主干逻辑
也许有更复杂的这种对象的字节流的一个拼凑
你都会让你的主干逻辑变得非常的清真
我觉得这个就非常的非常舒服
少写了很多代码
其实有点就把很多这种细节
都藏到errWriter里面去了
实际上并不是说if err!=nil不好用
而是说我们需要有一些技巧能够把它消除掉
这个是消除error
但是我们介绍完消除error少写代码之后
要回到我们之前说的Error Types这个问题
它没有彻底解决我们刚才说的
更详细的上下文
然后更优雅的错误处理怎么办
我们先看一个例子
还记得我们之前提的auth代码
我们这个auth代码我们回过头来看一下
大家还记得吧
这个代码
如果authenticate返回错误
AuthenticateRequest它会将错误抛给调用方
如果我这个方法调用它
如果它报错我肯定是往上抛
同样的以此类推
最终会推到这个调用的一个顶部
比方说可能是我们HTTP入口的代码
那么程序的主体会把这个错误打印到屏幕
或者是日志文件中
但是打印出来的是什么
打印出来是什么代码
就是一个no such file and directory
打不出任何东西
没有这样的文件或者目录
或者说没有用户
打不出任何东西
因为你的错误是一层层往上抛的
你因为不想破坏所谓的Sentinel Errors
就是我们预定义的一些错误
假设相当于你底层的代码包
抛了一个预定义的错误
我们叫Sentinel Errors
类似像io.EOF
你抛了个这种错误
你往上抛的时候
别人可能会用==来做一些判定
所以你不敢用fmt.Errorf去包装
因为包装的一些更详细的上下文的时候
上面的==判定逻辑肯定报错
你只能一层层往上透传
透到最顶层打印
真的去接到日志的时候很尴尬
no such file and directory
是什么东西找不到
是什么东西没有不知道
没有任何详细错误信息
非常的尴尬
我们怎么办
然后我们开始鼓励大家
在任何报错的地方开始加上各种详细信息
我相信大家都容易想得到
就是你既然错误不明确
我肯定就是在日志到处打了
因为你没有生成错误的文件名
行号这些信息
没有导致错误的调用堆栈
就是你的调用堆栈的这种
堆栈的跟踪能力
我们知道异常
Java的try catch
一个Exception出来
你是能知道完整的堆栈信息的
找问题的时候非常简单
但是对go来说
没有任何导致错误的
调用堆栈的一些的跟踪的信息
很尴尬
这样的一个情况会导致不熟悉代码
或者是即便熟悉代码的人
被迫进入长时间的代码分割到处找
先看顶层的error怎么抛出来的
先看一下
然后再去看谁调用了它
它调用了谁
然后一层层去找
代码分割方式地去寻找
到底是哪个地方报的错
很浪费时间
然后你又遇到了
这种模式与Sentinel Errors这种模型
或者是type assertions类型断言不兼容
Sentinel Errors是预定义的
你必须返回这个特定的类型
然后type assertions它是断言
以强依赖它的MyErrors这个自定义类型
它都不兼容
甚至于你的assertions by behavior
通过行为断言也搞不定
因为你把错误值通过fmt.Errorf变成一个新的error
它的字符串合并了
你的原始错误的字符串
和我用fmt.Errorf
把这个字符串合并完之后
返回了一个新的error
它破坏了原始的错误
导致等值判定会失败
这种情况非常尴尬，怎么办
我们再来看看一下一个有意思的事情
而我们先看一段话就
you should only handle errors once
你只能发现错误之后
你最好是只处理一次
那么Handing an error
means inspecting the error value
and make a single decision
如果你遇到了一个错误
你应该只做一件事情做一个决定
就是包括我自己也干过这种事情
我会经常在我们的业务的代码里面
发现这样的一个情况
就是到处打日志
我们看一下这个代码有什么问题
这个WriteAll先是调Write方法报错了
然后if err!=nil
这个时候第一个先打印错误
先记录一下行号
具体是什么传参报的错
把它打到日志里面去
然后再把它return err
再把它抛给调用者
我们再回想一下这句话
You should only handle errors once
你应该只处理错误一次
在这个地方我们处理了两次
第一个是打印了日志
第二个是把这个错误往上抛
我们再看看这段话
如果你判定了这个错误
你应该只做一个决定
要么打日志
打日志就意味着什么
假如说你抛了个错
这个错我做一个判定
你打日志意味着这个错误我了解了
相当于我调一个方法
这个时候我需要降级
我return nil了
因为这个错误我知道了我知晓了
所以说这个代码其实不好
而且如果你的代码一层调一层
一层调一层
每个地方它都会去把这个日志记录起来
所以在这个过程中
我们看一下这个是WriteAll报错的
假设你又有个WriteConfig
我们看一下先是调json.Marshal
这个WriteConfig会json.Marshal
把你的配置文件序列化
然后你这个时候又是做了两件事情
先是打印的日志
然后再把这个return err返回了
就是我们刚刚说的上面那段话
说的意思就是说你要么把这个错误往上抛
你要么不做任何事情
发现这个错误
抛出来了
然后我能够解决它
降级完之后应该是return nil
但是你现在其实做了两件事情
一个是又打了日志又return err
这个其实是不好的
我们再看一下
json.Marshal失败你记日志
然后return err
然后你又调了WriteAll
我们记得WriteAll里面是不是也记录了打了日志
return err
然后WriteAll又报错
WriteAll报错之后你又记录了一个日志
就could not write config又往上抛
其实它就是一层一层一层一层又返回给了调用者
那么调用者还会再记录一次日志
一直返回到程序的最顶端
到程序的最顶端和抛错误的这个地方
大家都是先记个日志
把错误往上抛
上面的人调你这个方法也是
你报错了怎么办
我只能记个日志往上抛
大家都在做重复的事情
所以我们再回想一下刚刚说的一句话
you should only handle errors once
只处理一次
现在很尴尬
如果不记日志的话又找不到
为什么报错
最顶层的话
我刚刚说的可能就是收到一个报错
比方说file not exist
我们刚刚说的这种错误
No such file and directory
报个这样的错误
你也不知道发生了什么
但是如果你这个时候去看日志的时候
它是散在各个地方打的日志
不完整
你如果是真的是这样打日志
你去线上查看问题的时候
你肯定是这些日志有可能都不连续
因为我们日志虽然是一个方法打下来的
但是你被集中式采集到以后
它可能就割裂在各个地方了
你再去找的时候其实就找不到
整个信息是割裂的
不像异常一样
有个完整的堆栈信息非常方便
所以我们再看看PPT的标题是叫什么
叫Wrap errors
我要把它包装起来
我们再看一下这个地方
不包装的情况下
你最顶部的打印
就是一个普普通通的io.EOF
抛了个这样的错尴尬的不行
怎么去把它包装起来
讲到包装其实也是一个非常特殊的情况
找到 Dave(Dave Cheney) 的一篇文章
专门讲error的包装
实际上在Go的处理里面
它是拖到了Go的1.13版本
才正儿八经的考虑包装error
因为如果没有包装error
是看不到完整的堆栈信息的
是非常麻烦的
虽然现在Go1.13也没有处理得非常好
我觉得基本上要拖到Go的2.0
可能会变得更友好
所以我们这个时候可能要借助第三方库
第三方库的pkg/errors这个包
后面我也会提到
在出现错误的情况下
我们不能对它的返回值内容做任何假设
我们在刚开始讲error的时候就讲过这个问题
我们再看看这个代码
如果json序列化失败
buf的内容是未知的
它有可能不包含任何内容
更糟糕的情况下它可能是包含json
包含了一小片
它没写完
它只有一小半的
只有半片的json片段在里面
这个就很尴尬
如果程序员在检查并记录错误后忘记return
为什么我们刚刚说的就是你遇到检测错误之后
你就应该做一件事情
要么直接往上抛
要么你就是做一些什么所谓的降级逻辑
你有没有可能你在打日志的时候
并且如果还做了一些别的逻辑
你就会忘记return有没有可能
可能在打完日志以后
就写完日志代码之后
你这个地方忘记return了
然后这个损坏的缓冲区buf
就传递到WriteAll
那么这WriteAll有可能会成功
因为你只是半片的json
并没有说WriteAll会失败
那么WriteAll的错误的配置文件就会被写入
最终的这个函数还会报的是正确的
因为这个地方没触发
刷盘成功return nil了
所以这个是有副作用的
为什么让大家遇到错误
你不能处理直接往上抛
如果你能处理就处理
最终能处理的情况下
应该是return nil往上抛
return nil就是我这个错误被我handle住了
没有遇到任何问题
然后我可以代码接着处理
或者说return nil就是被我handle住了
做些降级的逻辑这是没问题的
我们再来想一想打日志这个问题
因为我们刚才说了你的日志到处在打
散在各个地方
如果日志记录与错误无关
且对调试没有帮助的信息
都应该被视为是噪音的日志
你记的日志跟错误没有任何关系
对调试没有帮助
应该是噪音
应该这种日志要被质疑
那么记录日志的原因是因为某项东西失败了
而日志包含了答案
这个是我们最建议记日志的方式
这与我来说我通常几种情况下会记日志
我们的一个HTTP请求或者RPC请求输入输出的
它请求的request的一些参数
我会记录是一个什么样的人
对一个什么样的接口
做了一个什么样的事情
这个接口最终返回是成功还是失败
这种日志我会记录下来
为什么
它对我调试是有帮助的
哪天我要去排查某个用户
触发了什么行为
不管它是成功还是失败
对于我的调试是有帮助的
那么如果这个接口报错了
我会把错误信息也放到同一行这个日志里面
是不是对我是有帮助的
我们不应该在代码里面
散在各个地方都去打error
虽然它也是包含了错误信息
但是对你的调试帮助不大
因为它割裂了
我们看几个记日志的一个点
第一就是说错误要被日志记录起来
报错一定要记录日志
第二应用程序处理错误
应该保证100%的完整性
也就是说像json失败这种情况
你只能把这个错误往上抛
不能继续执行
你这个必须是处理错误的调用者应该要去考虑的
第三之后不应该再报告当前错误
如果产生错误地方
你记了日志
那么你往上抛
之后都不应该再去记录错误
否则你就是每个地方都在打日志
那就是要回到我们的
今天最重要的一个内容叫Wrap errors
大家先花10到20分钟
研究一下这个库怎么使用
pkg/errors这个库怎么使用
咱们再继续学习
先看一下这个库
它解决是什么问题
然后再来看我们的内容
我们刚刚是写配置
现在我读配置
假设我现在有个方法叫ReadFile
我去打开一个文件
因为我们刚刚是写配置
我现在是要读配置
我打开这个文件报错
我会return一个nil字节流
肯定是没有任何字节流的
我这个时候用这个errors Wrap
这个Wrap包它有什么功能呢
它其实是会把我们原始错误
既然是包装
原始错误就把你保留了
大家就先把errors Wrap理解为原始错误保留了
并且我还能捎带一些扩展的信息
比方说open failed
打开文件失败
我return了
这个是ReadFile
我们再来看一下
有个ReadConfig
因为ReadConfig呢
它会去调ReadFile
如果ReadFile报错
我会怎么做
我们这个地方看注意
有个叫errors.WithMessage
然后会把这个error带进去
然后会报一个叫could not read config
它这个地方又有个叫WithMessage的一个方法
这两个方法有什么区别呢
我们刚刚说了Wrap包装
它是会把底层根因
把底层的error
就原始的error给保留起来
同样的WithMessage
跟Wrap没有任何区别
但是Wrap是会把
当前产生错误的堆栈信息保存起来
WithMessage是不会再保存堆栈信息的
为什么
因为ReadFile已经保存了堆栈信息
我这个地方再去保存一次堆栈信息没有意义
因为它最底层的错误是这个地方抛出来的
附加了一些message
所以它叫WithMessage
我们再看一下这个main函数
在main函数调ReadConfig
如果报错我们看一下
这个original errors
它会去把这个错误的内容给打出来
注意它是有两种打印方式
一种是我们看它一共两行
第二行叫stack trace
我们的堆栈的追踪
它有一个叫%v的一个特殊的标记
能够把它堆栈信息打出来
就是说如果ReadConfig
我拿到这个error以后
我可以把它的完整的堆栈信息给拿出来
我们再看看它具体是长什么样的
就是errors.Cause是什么呢
errors.Cause相当于获取根因
我们刚刚Wrap是什么
包装根因
把原始错误包装起来返回回去
我们的Cause相当于是获取根因
获取原始错误
所以我们第一行打印original error
就是原始错误应该是什么os.PathError
原始错误肯定是os.PathError就是路径错误
我们注意它是%T
%T就是type
它这个是pkg/errors内部的一个实现
%T是它的type
那%v是它的详细内容
叫open某一个路径失败了
然后抛了一个错
叫no such file or directory
这样的话你是不是就明细的多
就既能拿到原始类型
因为errors.Cause根因这个方法
它会返回原始error的类型
那么你就又可以使用
我们的Sentinel Errors判定
以及你类似说像我们前面讲的error types
这种断言都能使用了
因为你拿到了根因
是不是就非常方便了
两个问题都解决了
我们再看一下堆栈信息也能拿到
因为我们之前为什么要在每一个地方去打日志
因为你不知道错误从哪抛出来的
你肯定需要获取它原始的堆栈信息
那么通过%v
这个pkg/errors
它能够记录原始堆栈信息
我们下面所以你看stack trace这个地方
是完整的一个堆栈信息
非常的方便
这个就是pkg/errors这个包它带来的一个功能
但是这个包它使用上还是稍微有一些门槛
我们接下来讲一下
接下来讲一下就是它的一些使用的技巧
我们现在知道了通过使用pkg/errors这个包
往一个error里面增加一些上下文
这个上下文就是WithMessage这种上下文
这种方式既可以由人也可以由机器来使用
由人使用是什么
我们需要获取调试信息
整个问题的一些信息
人使用就是看到你error的详细的错误
为什么叫机器也可以使用呢
你的程序要使用这个error是使用什么
使用它的原始的错误是什么
它的根因错误是什么
你通过errors.Cause获取的根因
你不就程序也能使用了么
所以说人和机器都能使用
所以这个包这两个问题都解决了
再回到我们这个代码我之前说了
我们只应该处理错误只处理一次
你要么打印它
你要么直接往上抛
这个代码要打两次
就是处理两次这个怎么优化
我们看一下是不是非常简单
如果这个Write报错了以后
我们直接return errors.Wrap
把原始错误在这个地方记录起来
然后再加上一些扩展信息
比方说write failed写错误
这个地方把原始的错误给包装起来
我不记日志
我一层层往上抛
抛到哪
抛到我们最上面
比方说可能是我们net/http
或者如果大家使用gin框架
或者使用我们Kratos框架
或者go-zero框架
你可以在它的Middleware
就我们的中间件上
我们知道很多类似像gin http
你都可以去扩展它
加很多中间件
类似像before after hook这种方式
你可以在第一个中间件里面
把日志给加上
你就可以获取一个完整的请求链路的参数是什么
请求的哪个接口
成功还是失败
堆栈是什么
非常好用
如果报错的话堆栈信息会打出来
你去排查问题是非常方便的
通过Wrap这个方式
你在上层如果使用类似像Cause那个方法获取根因
获取它原始错误的方式
你能程序用来做什么
Sentinel Errors的等值判定
我们的哨兵error等值判定
因为它是预定义的
你要使用==
那么既满足扩展信息的阅读需求
又满足什么程序判定的逻辑
其实是基本就把我们所有问题都解决了
Wrap errors它有一些什么样的使用技巧呢
在我们的业务的代码里面
你可以使用这个errors.New
或者是errors.Errorf来返回错误
这个errors.New和errors.Errorf
它都是由pkg/errors这个包提供的
这两个方法其实都会携带上原始的堆栈的信息
就是哪个地方报的错
原始的堆栈信息它都会记录起来
这样的话比标准库的errors.New
它没有任何扩展信息的
堆栈信息拿不到
比较方便这是第一个
第二个如果调用其它包内的函数
你在你这个项目工程里面
如果你调用别人的函数有报错
通常都是直接返回
是不是就满足我们刚才说的只处理错误一次
你遇到别人报错了你肯定是直接往上抛
你处理不了
如果你能处理
一定是你调这个方法降级
这个时候你就不会return err了
你会继续往正常逻辑往下走
所以说它一共就两个
要么你往上抛要么你能处理
但是处理完之后
它不应该是个错误了
它应该是个主干正常逻辑往下走的
所以这是第二个如果调用其它包内函数直接往上抛
第三个
如果和其它库进行协作
请使用errors.Wrap
或者Wrapf来保存堆栈信息
其它库是什么呢
就是第三方库或者是标准库
你和第三方库或者标准库协作的时候
你可以使用Wrap把堆栈信息包起来
跟第一个有区别
第一个是你的业务代码里面
你发现一些这种错误
它不是别人导致的
是你自己因为一些逻辑
类似像这种什么参数不够
是你自己主动产生的错误
你需要把堆栈信息保存起来
使用errors.New或者errors.Errorf抛出来
这里的情况是你调别人
别人报错了
那么如果对方是标准库
或者是第三方的Github的这种库
你是可以使用什么Wrap
你使用Wrap把它包装起来
包装以后就有堆栈信息
你就可以继续往下走往上抛
这是第三个
第四是什么
直接返回错误
而不是每个错误产生的地方
到处去打日志
注意直接往上抛
刚刚我们讲过了
再下一个点是什么
在程序的顶部或者是工作的goroutine的顶部
工作的goroutine是指的是
比方你是个请求
请求的入口
使用%v的方式
把堆栈的详情给记录起来
举个例子这个地方
你可以使用%v把它堆栈详情给记录起来
你可以使用errors.Cause
Cause其实就是原因
获取它的root error
它原始的error
然后再进行和你的Sentinel Error去做判定
就是预定义好的error去进行判定
所以这个就是它pkg/errors
使用Wrap errors的一些使用的规则
我们总结再来看一下
第一点叫Packages that are reusable across
many projects only return root error values
这句话其实很有深意
我们有没有想过
如果一个GitHub的第三方库
它也使用了Wrap errors
你再去按照我们刚刚上面的一个规则
和其它库协作的时候
这个其它库包含标准库和第三方GitHub库
要使用errors.Wrap
errors.Wrap和Wrapf去保存堆栈信息
这就尴尬了
会保存两次堆栈信息
如果你去打印日志这个时候你会看到两坨堆栈
这个就很尴尬
两坨堆栈就是冗余了
所以这段话是什么意思
Packages that are reusable
across many projects
指的是如果你提供的库被很多人使用
并且是跨很多项目的
那么你不应该使用任何的Wrap
包装错误Wrap errors
你应该返回就是它的根因
它的原始的错误是什么就是什么
选择使用Wrap error
从我的视角来讲
我只建议在application去使用
就是指的你的业务的应用去使用
而不是说一个基础库
一个第三方库在里面使用
你的业务代码可以使用
你的第三方库不应该使用
它只能抛Sentinel Errors的原始错误
或者是自定义的错误
具有最高重用性的包只能返回根错误
一定要注意
这个机制其实跟Go的标准库使用是一样的
就Go的标准库比方说
sql.ErrNoRows
它肯定不会去返回一个包装的信息给你
因为你这个时候不知道什么情况下需要Wrap
所以这个第一点
第二个点
if the error is not
going to be handled
wrap and return up the call stack
这个是关于函数方法调用
返回的每个错误的一个基本问题
如果你的这个函数不打算处理这个错误
你没法处理
你肯定是携带上足够多的上下文
然后往上抛不去解决的
就是使用wrap errors
往上抛就好了
例如额外的上下文可以使用的输入参数
或者是失败的查询语句
比方说你做了一个SQL查询报错了
你肯定是把这个查询语句把它放进去
一般要脱敏
就是不能发一些什么密码错误那种具体的字段
可能是SQL放进去
然后把一些重点的
非敏感的数据放到这个里面去
把它wrap包含在调用堆栈里面往上抛
如果这个错误你能处理
那你就处理
如果你处理不了
肯定是带上足够多的信息往上抛
这是第二个
如何确定你记的
包含的上下文信息是否足够
其实就是最终你去看日志的时候
能不能帮你解决问题
比方说你不知道是哪个用户导致的
可能这个SQL里面有关于用户的ID
你可能要把它带进去
这样的话对你是有帮助的
还有一个Once an error is handled
it is not allowed to be passed up
the call stack any longer
如果这个错误被人处理过
就不应该往上抛了
并且去记日志
一旦这个函数将这个错误给处理掉
处理掉指的是指降级了
虽然它报错了但是它降级了
你不应该把原始的error再抛
因为我理解就是说
这种情况下你处理掉了
那你返回的降级数据
或者是需要return nil
或者是走主干逻辑往下继续走
而不是说继续往上抛
否则又会出现你的逻辑往下走
上层就记着一个错
你的逻辑有在往下走
这个其实是不好的
这个是它的三个使用的一个
总结的一个规律就是Wrap errors
包括我们记日志的时候非常重要的一些点
这一块我们基本上就讲完了
然后接下来我们虽然介绍了
pkg/errors的使用
但是我们还没讲Go1.13
那Go1.13的errors处理
会把这两者
就是pkg/errors这个包作者是Dave(Dave Cheney)
它的思路其实也被官方所得到认可
官方1.13当然它考虑向下兼容性
做了一些取舍
它没有完完全全按照他的思路去做
我会去把它们两者相结合的一些地方
再去展开讲一讲
我觉得可以非常期待一下Go2
会去怎么去解决这个问题
我们先到这里