-->

左耳听风_012_11_程序中的错误处理错误返回码和异常捕捉

你好,我是陈浩网名作耳朵house.今天呢我们来讨论一下程序中的错误处理,也许你会觉得这个事儿没什么意思,处理错误的代码并不难写,但是你想过没有啊,要把错错误处理写好,并并不是件件容易的事情。

另外呢任何一个稳定的程序中啊,都会有大量的代码在处理错误。

所以说处理错误啊是程序中一件比较重要的事情。

那这里呢我会用两节课来系统的讲一下错误处理,各各方式式相关关实实践。

先先给你提醒醒啊,这节课中呢会出现大量的示例代码。

我强烈建议你打开文章,配合着音频进行着学习,这样呢有助于更好的理解解。

好,接接下来我们正式开始我们的课程。

首先呢我们知道处理错误最直接的方式呢就是通过错误码,这也是最传统的方式。

过程式语言呢通常都是用这样的方式来处理错误的。

比如说c语言,基本上来说它都是通过函数的返回值来标志是否有错误。

然后呢通过全局的error number变量,并且配合一个error string的数组来告诉你为什么出错。

那为什么是这样的设计呢?道理很简单,除了可以共用一些错误,更重要的是这其实是一种妥协。

比如说read right open这些函数的返回值啊,其实是返回有业务逻辑的值。

也就是说这些函数的返回值啊有两种语义,一种呢是成功的值,比如open函数返回的文件,句柄指针、file指针。

另一种呢是表示错误的值。

比如我返回了一个now,这样呢会导致调用者并不知道是什么原因导致出错了,需要去检查error number来获得出错的原因。

这样呢才可以正确的去处理错误。

那一般来说呢这样的错误处理方式啊在大多数情况下都是没什么问题的。

但是也有例外的例子,比如c语言里面啊,有一个叫a to i的函数。

那这个函数的作用呢是把一个字符串转换成相应的整形数字。

那它的输入啊是一个表字字,串串指针输出呢是一个int.但是问题来了,假如说我一个要传的字符串是非法,比如说传了一个ABC,或者是传了一个整形溢出了,那么我这个函数应该返回什么呢?出错的返回呢,返回什么数都不合理,因为这样呢会和正常的结果混淆在一起。

比如说我出错了,我就返回一个零,那这样的话我就没办法区分,我是因为出错而返回的零啊,还是传了一个正常的表示零的字符串而返回零。

你可能会说啊,是不是需要检查一下error number的变量,讲道理其实是应该需要检查的。

但是在c九九的规格说明书中呢,却提到像a to i啊a to FA to l或者是a to l啊这样的函数啊,是不会设置l number的。

比且啊说明书还提到,如果结果无法计算的话,行为是any finfind.所以后来呢lib c又给出了一个新的函数string to long.那这个函数除了接受一个字符串返回一个long类型的转换结果以外呢,它还需要你提供一个出参用于存放字符串转换之后剩余的部分,并且在出错的时候呢会设置全局变量l number.那这样的话,我们就可以正确的去处理错误了。

我们可以通过比对字符串转换前后是否有变化来判断是否转换成功了。

那对于转换失败的情况呢,我们也可以检查error number的值来做相应的处理。

于是呢也给出了相应的代码。

那这样呢string to lolong函数就解决了a to i函数的问题。

那尽管如此呢,我们还是能感觉到不是很舒服和自然。

所以这种用返回值加error number的错误检查方式啊,会有一些问题。

首先呢程序员一不小心就会忘记返回值的检查,从而造成代码的bug.其次呢函数的接口非常不纯洁,正常的值和错误的值混淆在一起导致语义的问题。

所以后来呢有一些类库就开始区分这样的事情。

比如windows的系统调用,就开始用h result的返回来统一错误的返回值。

那这样呢我就可以明确函数调用的返回值是成功还是错误。

但是这样一来,函函数的输入和输出都只能通过函数的参数来完成。

其次呢出现了所谓入参和出参参量的问题,但这样的话又会让函数中的参数的语义变得很复杂。

有一些参数是入参,有一些参数是出参,而且啊依然没有解决函数的成功或失败,可以被人为忽略的问题。

于是呢有一些语言通过多反馈值来解决这个问题。

比如说go语言构元的很多函数呢都会反馈result和error两个值。

这样的话参数呢基本上就只有入参了,而返回接口呢会把结果和错误分离开,这样呢就会让函数的接口语义啊变得更加清晰。

而且啊构语言中的错误结果如果要忽略需要显示的忽略,比如说用下划线这样的变量来忽略。

另外呢因为返回的error是一个接口,所以你可以扩展自定义的错误处理逻辑。

啊。

这里呢我给你举了一个jason语法错误处理的例子。

这个事例呢来自go的官方文档,error handling and go.如果你有时间呢,可以点击链接细看。

另外呢如果果个函函会返回多个不同类型的erroror,我们可以在处理错误的时候啊,针对error变量具体的类型来执行不同的逻辑。

我在文章中呢也给出了相应的代码。

但是啊即使像go这样的语言啊,能让错误处理的语言更清楚,而且还有可扩展性。

但是它也有一些问题,如果你写过一段时间的go语言,你就会明白其中的痛苦。

If error不等于new这样的语句啊,简直是写到to只能在IDE中定义一个自动写这个代码的快捷键,而且正常的逻辑代码会被大量的错误处理打的比较凌乱。

那接下来呢我们再谈一下出错后资源清理的问题。

程序出错的时候呢,一般都需要对已经分配的一些资源做清理。

传统的做法呢是每一步的错误,都需要去清理前面已经分配好的资源。

于是呢就有了go to file这样的错误处理模式。

也就是说我在一个方法的最下面把前面申请了,所有资源都统一free掉。

那前面任何一步有问题呢,都会通过go to来跳转到free这一段语句。

那这样的处理方式啊虽然可以,但是会有潜在的问题。

那最主要的一问题呢,就是你不能在中中间代码码出现return n为你需要在最后后面清理资源。

所以在维护这样的代码的时候候,我们需要非常小心,在一不注意呢就会导致代码有资源泄露的问题。

于是呢c加加的RARI机制呢,使用面向对象的特性可以很容易的去处理这个事情。

我们可以利用c加加类的机制,在构造函数中去分配资源,在析构函数中呢去释放资源。

我举个例子啊,假如说我们在函数中有一个需要加锁的逻辑,在函数开始的时候呢,加锁在函数结束的时候去解锁。

呃,这样的话万一中间出错了,需要提前返回解锁的逻辑啊,就再也走不到了,就会出现死锁的问题。

但是用III呢,我们可以声明一个类在构造函数中去加锁,在析构函数中呢去解锁。

这样不管函数是正常返回还是出错,返回这个类的析构函数啊,都会调用,就可以解决思锁的问题了。

在go语言中呢,我们使用关键字也可以做到这样的效果。

不知道从上面这三个例子来看,不同语言的错误处理,你自己更喜欢哪个呢?就代代码易读读性和干程而言呢,我更喜欢defc加加的III模式。

首先是go的deffer模式,最后呢才是c语言的go to faile模式。

刚刚呢我们讲了错误检查和程序出错后,对资源的清理这两个事儿能把这两个事做的都比较好的。

其实是try catch finfinally这个编程模式,它可以把正常的代码错误处理的代码,资源清理的代码分门别类,他看上去非常干净。

有一些人呢就明确表示不喜欢try catch这种错误处理方式,比如stack or flow的CEO, joe sports YY.但是我还是想说一下ccasch finally这样的异常处理方式有这么几点好处。

首先呢函数接口在入参和返回值以及错误处理的语义啊是比较清楚的。

同时呢正常逻辑的代码可以和错误处理和资源清理的代码分开。

首先了代码的可读性,另外呢异常不能被忽略,如果要忽略呢,也需要catch住,这就是显示忽略。

还有一个好处啊,是在面向对象的语言中呢,异常也是个对象,所以可以实现多态式的catch与状态。

返回码相比呢异常捕捉。

还有一个明显的好处就是函数可以嵌套调用或者是链式调用。

当然你可能会觉得异常捕捉对程序的性能是有影响的这句话呢也对也不对。

原因是这样的,这方面来说呢,异常捕捉的确是对性能有影响的。

但是因为一旦异常被抛出来,函数也就跟着return了。

而程序在执行时呢,需要处理函数栈的上下文,这就会导致性能变得很慢,尤其是函数栈比较深的时候。

但从另外一个方面来说呢,异常的抛出啊基本都是表明程序的错误。

程序在绝大多数的情况下,应该是在没有异常的情况下去运行的。

所以有异常的情况应该是少数的情况不会影响正常处理的性能问题。

总体而言呢,我还是觉得try开finally这样的方式是很不错的,而且这个方式啊比返回错误码在很多方面都更好一些。

但是try开finfinally有个致命的问题,那就是在异步运行的世界里。

如果try里面的函数运行在另外一个线程中,那么它抛出的异常就没办法在调用者所在的线程中被捕捉到了,那这个问题就比较大了。

我们异常捕捉呢,我想拿它跟返回错误码比较一下。

我们在处理错误的时候呢是返回错误状态,还是用异常捕捉,可能是一个很容易引发争论的问题。

有人说啊对于一些偏底层的错误,比如说控制帧啊、内存不足等等,可以使用返回错误状态码的方式。

而对于一些上层的业务逻辑方面的错误啊,可以使用异常捕了。

那这么说有一定的道理,因为偏底层的函数啊可能会用的更多一些。

但是我并不这么认为,前面我们也比较过两者的优缺点。

总体来说呢,似乎异常捕捉的优势更多一些。

但是我觉得应该从场景上来讨论这个事儿才是正确的姿势。

要讨论场景呢,我们需要先把要处理的错误分好类别,这样呢有利于简化问题。

因为错误其实是很多些不同的错误需要有不同的处理方式。

但错误处理呢是有一些通用规则的。

为了讲清楚这个事儿呢,我们需要把错误分类。

我个人觉得错误啊可以分为三个程序。

第一类呢是资源的错误,就是我们的代码去请求一些资源的时候导致的错误。

比如打开一个没有权限的文件,写文件的时候出现了写错误。

比如发送文件到网络端,发现了网络故障的错误等等。

那这一类错误呢属于程序运行环境的问题。

那对于这类错误呢,可的我们可以处理,有的我们就无法处理。

比如内存耗尽啊,占溢出啊,或者是一些程序在运行的时候,关键性资源不能满足等等。

这些情况我们只能停止运行,甚至退出整个程序。

第一类呢是程序的错误,比如说控制帧啊,非法参数等等。

那这一类呢是我们自己程序的错误,我们要记录下来写入日志,最好呢是触发监控系统的报警。

第三呢是用户的错误,比如by request, by format等等。

这类因为用户不合法的输入带来的错误。

那这类错误呢,基本上是在用户的API层上出现的问题。

比如说我解析一个XML或者是deason文件,或者是用户输入的字段不合法之类的那对于这类问题呢,我们需要向用户端报错,让用户自己去处理,修正他们的输入或操作。

然后我们正常执行,但需要做统计,统计相应的错误率。

这样呢有利于我们改善软件或者是侦测是否有恶意的用户请求。

我们可以看到啊,这三类错误,有些是我们希望杜绝发生的。

比如程序的bug,有些呢是我们杜绝不了的。

比如说用户的输入,而对于程序运行环境中的一些错误呢,是我们希望可以恢复的。

也就是说我们可以希望通过重试或者妥协的方式来解决这些环境的问题,比如重建网络连接啊,或者是重新打一个新的文件啊等等。

所以我们可以这样在逻辑上分为两类。

第一类呢是对于我们并不希望会发生的事儿,我们可以使用异常捕捉。

那第二类呢是对于我们觉得可能会发生的事儿,我们我就可以使用返回码。

比如说你的函数参数传入的对象不应该是一个闹对象。

那么一旦别人传入了一个闹,那函数就可以抛异常,因为我们并不希望总是会发生这样的事情。

而对于一个需要检查用户输入信息是否正确的事儿呢?比如电子邮箱的格式,我们用返回码可能会更好一些。

所以对于上面三种错误的类型来说呢,程序中的错误可能用异常捕捉会比较合适。

那用户的错误呢用返回码比较合适,而资源类的错误要分情况,因为异常捕捉还是用返回值,要看这事儿是不应该出现的,还是经常出现的。

呃,当然了,这只是一个大致的实践原则,并不代表所有的事儿都需要符合这个原则。

除了用错误的分类来判断返回码还是异常捕捉之外呢,我们还要从程序设计的角度来考虑哪种情况下使用异常捕捉更好,哪种情况下使用返回码更好。

因为异常捕捉在编程上的好处啊,比函数捕捉值好很多,所以还使用用异常捉捉代码会更易读,也更健壮壮些。

而而反回呢是不容易被忽略,略所使用返回回码代码需需做做测试才能得到更好的软件质量。

不过啊我们也要知道,在函种情况下呢,你只能使用其中一个。

首先呢是在c加加承载操作符的情况下,你就很难使用错误,返回码只能抛异常。

其次呢,异常捕捉只能在同步的情况下使用。

在异步的模式下呢,抛异常这事儿就不行了,需要通过检查子进程退出码或者是回调函数来解决。

那还有就是在分布式的情况下,那调用远程服务只能看错误返回码,比如HTP的返回码。

所以在大多数情况下呢,我们会混用这两种报错的方式。

有时候呢我们还会把异常转成错误码,也会把错误码转换成异常。

总体来说呢,报错的类型和错误处理是紧密相关的,错误处理方法多种多样,而且呢会在不同的层面上去处理错误。

有些底层错误呢就需要自己处理掉。

比如底层模块会自动重建网络连接,而有一些错误呢需要更上层的业务逻辑来处理。

比如说网络连接重试不成功之后,只能让上层业务来处理,怎么办?是降级使用本地缓存呢,还是直接报错给用户呢?所以不同的错误类型,再加上不同的错误处理,会导致我们代码组织层面上的不同,从而会让我们使用不同的方式。

也就是说,使用错误码还是异常捕捉,主要还是看我们的错误处理流程和代码组织怎么写会更清楚。

那通过学习今天的内容,你是不是已经对怎样处理程序中的错误,还有在不同情况下,怎样选择错误处理方法有了一定的认知和理解呢?但这些知识和经验呢,仅仅在同步编程的世界中适用。

因为在异务编程世界里呢,被调用的函数啊是被放到另外一个线程中去运行的。

所以这节课的两位主角,不管是错误返回码还是异常捕捉,都很难发挥他们的威力。

那么异步编程的世界中是怎样做错误处理的呢?我们将在下节课里去讨论。

同时呢我还会给你讲一讲我在实战中总结出来的错误处理的最佳实践。