左耳听风_013_12_程序中的错误处理异步编程以及我的最佳实践
你好,我是陈浩网名做耳朵house.在上节课中呢,我们讨论了错误返回码和异常捕捉这两种错误处理的方式,以及在不同情况下应该怎样选择和使用它呢?在这节课中呢,我会接着讲两个有趣的话题,分别是在异步编程世界里的错误处理方法。
还有呢就是我在实战中总结出来的错误处理的最佳实践。
这节课呢同样会出现大量的代码,如果你不看文章呢,很可能会听不懂。
所以我还是推荐你打开文章,配合着音频去学习。
嗯,好了,正式开始。
我们这节课的学习。
在异步编程的世界里呢,因为被调用的函数啊被放到了另外一个线程里运行。
那这样的话在错误处理上呢会导致两个问题。
那第一个问题呢,是我们没有办法使用返回码了,因为函数被放到了另外一个线程去异步运行。
那所谓的返回呢,只是把处理权交给了下一条指令,而不是把函数运行完的结果返回。
所以函数返回的语义完全变了,那返回码也没有用了。
那第二个问题呢,是我们也没办法使用抛异常的方式。
因为除了刚刚所说的函数立马返回的原因之外呢,抛出的异常也在另外一个线程里面。
那不同线程中的战士完全不一样的。
所以主线程的catch方法完全看不到另外一个线程中的异常。
所以在异务编程的世界里呢,我们会有好几种其他的处理错误的方法。
那最常用的呢就是靠带方式。
在做异务请求的时候呢,我们注册几个on success on failure这样的函数,让它在另一个线程中运行的异步代码呢回调过来。
我们先来看看javascript里面异步编程的错误处理。
比如在文章中这个javascript t代码示例中呢,我们可以先注册一个错误处理的回调函数。
那这样异步执行的函数在出错的时候呢,就可以调用这个回调函数。
那这样的方式呢比较好的解决了程序的错误处理。
而出错的语义呢,从返回异常捕捉到了直接耦合错误,处理函数的样子看上去还不错。
但是呢假如说我有好几个异步函数啊,因为异步程序执行的顺序是不确定的那有的时候呢这几个异步函数会有上下文相互依赖的情况,所以我希望他们可以按顺序执行。
那在这种情况下呢,如果我还用回调函数去处理错误的话,就会出现像文中示例这样的call by hell,也就是回调地域的问题。
而在这样的层层嵌套中呢,我们要注册的错误处理函数啊有可能是完全不一样的。
这样呢就会导致代码非常的混乱,很难阅读和维护。
所以一般来说呢,在异步变程的时间里面啊,我们会用promise模式来处理。
那以文章中展示的代码为例子,对于一个异步执行的方法呢,我可以让它返回一个promise.这promise呢有一个zn方法和一个catch方法。
在zn方法里面呢,我可以把各个异步的函数给串联起来。
而catch方法呢则是出错的处理。
那为了改造前面层层嵌套的级联式的调用方式呢,我们需要让这些异步的方法都返回promise对象。
在文中呢我放了代码的例子。
从这个例子中呢,我们可以看到啊,如果成功了,就调用promise点ressult方法,这样这个promise对象就会继续调用下一个。
Then那如果出错了呢,就调用promise点reject方法,那这样呢就会忽略后面的one.而直接到catch方法,我们可以看到promise reject就像是抛一场一样。
那这个变声模式啊让我们的代码组织方便了很多。
另外呢这里我多说一句啊,promise还提供了一个promise点wan方法,它可以让我们同时等待两个不同的异步方法。
这里呢建议你回到原文去看一看具体代码的展示。
在ECMA scrip t二零一七的标准中呢,我们还可以使用a sink和await这两个关键字来取代promise对象。
这样呢可以让我们的代码更加好读。
比如我在函数定义之前呢,使用了一个assink关键字,那这样的话我就可以在函数里面去使用await.而当我在使用await去等待某个promise的时候呢,这个函数就会暂停执行,暂停到这个promise产生结果。
而且啊这个暂停还不会阻塞主线程。
如果promise调用了resolve啊,就会返回值。
如果promise调用reject呢,就会抛出拒绝的值。
这样的话我们的义务代码呢完全可以放在一个track ch的语句块里面。
在有了语言知识以后呢,我们又可以使用track catch语句块了。
文章里呢还有一段pipeline的代码。
那所谓pipeline呢就是把一串函数给编排起来,从而形成更为强大的功能。
这个玩法是函数式函程中经常用到的方法。
那接着我们再来讲一下java异步编程中的promise模式。
在java中呢,JDK一点八里面也引入了类似javascript的玩法,也就是compleinable future这个类啊提供了大量的异步编程中promise的各种方式。
接下来呢我列举几个,首先是链式处理嗯,completable future提供了supply assink k and and apply.还有then accept的这些API.那这样呢就可以让链式处理和javascript中的ln方法很像。
那其中呢supply a sink表示执行一个异步方法,而then apply啊表示执行之后再串联另外一个异步方法。
最后呢是deal accept来处理最终的结果。
那除了链式处理呢,它还提供了then combined APPI可以用来合并两个异步函数的结果。
那接下来呢completable future还提供了一个ceptionally方法。
那这个呢和javascript中promise点cash方法类似,在文章中的代码里啊,你还可以看到啊,completable future还提供了一个handle方法,可以让我们去处理最终的结果。
其中啊也包含了异步函数中的错误处理。
那接下来呢我们再来谈一谈构语言中的promise.在购员呢。
如果你想实现一个简单的promise模式啊,也是可以的。
我在文章中给出的代码只是个例子,只是为了说明问题。
那如果你想要更好的代码,可以上giyhub上搜一下go语言promise相关的代码库。
首先呢我先声明一个结构体,那这个结构体里面有三个成员啊,第一个weit group p用于多线程的同步,第二个成员result用来存放执行的结果。
第三个error呢用来存放相关的错误。
然后呢我定一个初始化函数来初始化这个promise对象。
那其中可以看到啊,需要把一个函数f传进来,然后调用v group点add方法。
对,v group做加一的操作之后呢,我们新开一个go routing,通过异步去执行用户传入的函数f然后记录这个函数的成功或者错误,并且啊把v group做简易操作。
呃,接下来呢我们再来定义promise的dan方法,而这个dan方法需要传入一个下一步要执行的函数和一个错误处理的函数,并且啊要调用we grow点点way的方法阻阻塞。
那旦上一一个方法被调用了group点down,那那个个an方法就会被唤醒。
那唤醒之后的第一件事儿呢,就是检查一下之前的方法有没有错误,那如果有呢,就调用错误处理函数。
那如果之前成功了,就把之前的结果呢以参数的方法传入到下一个执执行函数中。
那这样的话我们的promise模式啊就设计好了。
那为了测试呢,我们定义一个异步方法,那这个方法很简单,就是在数数,然后呢有一半的几率会出错。
那接着呢我们再来看看我们实现的购物员的promise是怎么使用的。
文中的代码还是比较直观的,我就不做更多的解释了。
那当然如果你需要更好的购语言promise啊,可以到gaay hang上面去找文中好些代码都实现的很不错的那刚刚这个事例呢实现的比较简陋啊,只仅仅是为了说明问题。
那接下来呢是我个人总结的几个错误处理的最佳时践。
如果你知道有更好的,请一定要告诉我。
那首先第一条呢是要建立一个统一分类的错误字典。
无论你是使用错误码还是异常捕捉啊,都需要认真,并且统一的做好错误分类。
最好啊是在一个地方去定义相关的错误。
比如在HTTP里面啊,四开头的错误码就表示客户端有问题,而五开头的错误码就表示服务端有问题。
也就是说你要建立一个错误字典。
那第二呢是同类错误的定义啊,最好是可以扩展的那这一点呢非常重要。
而对于这一点呢,通过面向对象的继承,或者像go语言那样的接口多态都可以很好的做到,这样呢可以很方便的重用已有的代码。
第三呢,我们要定义错误的严重程度。
比如fatal就表示重大错误。
Error呢就表示资源或者是需求得不到满足。
Warning呢表示它不一定是个错误,但还是需要引起错息。
Info呢表示,这不是错误,只是一个信息。
而第bug呢用来表示这是给内部开发人员用调调试序序的那接来来第四条经验呢是错误日志输输出,而不是使错错码,而不是错误错息。
我们在打印错误日志的时候啊,应该使用统一的格式,但是最好不要用错误信息,而应该使用相应的错误码。
而错误码不一定是数字,也可以是一个能从错误字典里面找到的一个唯一的可以让人读懂的关键字。
而这样的做法呢会非常有利于日志分析软件进行自动化监控,而不是要从错误信息中做语义分析。
比如HTTP中的日志啊,就会有HTP的返回码。
比如四零四啊,但是我个人更推荐使用像配置note fund这样的标志,这样人和机器啊都会很容易的去处理。
第五条经验呢就是你在忽略错误的时候啊,最好要打个日志,不然呢会给后续的维护带来很大的麻烦。
第六条经验呢是如果同一个地方在不停的报错,最好不要都打到日志里面,不然这样会导致其他日志被淹没了,也会导致日志文件太大。
那这种情况下,最好的实践呢是打出一个错误,以及它出现的次数。
那第七条呢是不要用错误处理的逻辑来处理业务逻辑。
也就是说,我们不要使用异常捕捉的方式来处理业务逻辑,而是应该使用条件判断。
如果一个逻辑控制可以用if else清楚的表达,那就不建议使用异常的方式去处理异常捕捉啊,是应该处理不期望发生的事情。
而错误码呢则用来处理可能会发生的事。
第八条经验呢是对于同类的错误处理,我们要用一样的模式。
比如对于now对象的错误,要么呢都返回一个now,加上条件检查,要么都抛出一个now, pointer exception不要混用,这样呢有助于代码的规范。
第九是尽可能在错误发生的地方去处理错误,因为这样的话会让调用者变得更简单。
那第十条呢我们要向上尽可能的返回原始的错误。
也就是说如果我一定要把错误返回到更高层去处理啊,那么应该返回原始的错误,而不是重新发明一个新的错误。
那第十一条经验呢是处理错误的时候啊,总是要清理已经分配的资源。
那这一点啊非常关键。
我们使用RAII技术或者是try care finally,或者是go differ都可以很容易的做到这一点。
第十二条呢是不推荐在循环体里面去处理错误啊,这里呢我说的是try cash,那绝大多数的情况你都不需要这样做。
嗯,最好呢是把整个循环体啊都放在try里面,而在外面去做catch第十三条呢是不要把大量的代码都放在同一个try语句块里面。
那一个try语句块里面的语句啊,应该是完成一个简单单一的事情。
那第十四小经验呢就是为你的错误定义啊,提供一个清楚的文档和每一种错误的代码示例。
那如果你是做rrestful API方面的话,使用swagger呢会帮你很容易的搞定这个事儿。
还有一条经验呢就是对于异步的方式啊,我们推荐使用promise模式来处理错误。
那对于这一点呢,javascript中啊就有很好的实践。
呃,最后呢就是对于分布式的系统啊,我们推荐使用APM相关的软件,尤其是要使用zip king这样的服务调用跟踪的分析工具来关联错误。
好了,那关于程序中的错误处理呢,我主要就总结了这些。
如果你有更好的想法和经验呢,欢迎来留言区跟我交流。