关于error handling的一些理解

前言

  一晃四五个月过去,发现好久没写博客了。一是因为最近这几个月事情比较多,二可能是因为懒了吧(毕竟动脑子的东西,相当耗能量和脑细胞),╮(╯▽╰)╭ 。 不过还是要克服一下,尤其是还在段位比较低,还需要持续成长的过程中。

  对于程序员来说,错误处理是不可避免的,这次简单谈谈实际编程过程中的Error Handling。
在这里插入图片描述

  

一、error handing 的常见处理方式

  这里所谈的错误处理不区分有些语言中的error和exception,都视为程序中遇到的不正常的情况。也就是和预想的正常执行流不一样情况。这些情况有可能是因为程序本身的逻辑出了问题,比如说,非法除以0,数组越界等等; 也有可能是因为获取资源时暂时不可得,如网络请求,内存请求等; 还有可能直接是硬件的问题,突然掉电,服务器崩溃等等。

  那在程序中遇到可能发生异常的情况怎么办呢?
  按照我的理解,大体来说有三种方式:

在这里插入图片描述

  • 忽略错误
      对于这种情况,大多是一些不太重要的错误, 有没有错,一点也不影响程序执行。但这种情况一般比较少见,能谈及的错误,一般怎么都需要稍微处理一下的。

  • 处理错误
      这是最常见也是最复杂的情况,因为遇到的情况多种多样,处理的方式也自然不能一概而论。
      一般来说,都需要做的一个步骤就是:log 一下,记录一下出错的原因。

    还有就是重试, 这种情况主要是用在逻辑本身就可能有很大的可能出现不成功,比如说资源的请求, 尤其是网络资源的请求。因为网络这个东西,你懂得,出现failure的概率是很大的。对于这种情况,失败了一次就放弃了往往不是很好的选择(除非资源相当不重要),大多数情况下还是需要重试几遍的。

    还有一点需要注意的就是,在某些情况下,需要考虑整个业务是否具有一些事务性的特点,是否需要保证数据的一致性。如果前期已经有一部分数据处理完毕(可能是发送、写入等等),这个时候出错误了,就需要考虑接下来的处理是否会影响到数据的一致性(更确切的说是业务的一致性),如果重试的话,操作是不是幂等的。 如果放弃的话,前期的处理的数据会不会带来业务上的混乱。 这个其实和数据库事务的一致性很像(需要从一个一致性状态转移到另一个一致性状态)。
一般来说,大多数业务不需要这样高的一致性要求,如果需要的话,比如说金融业务,那就需要好好考虑这种情况了,是不是需要恢复到上一个状态,还是redo到下一个状态。

  • crash
      如果无法处理,或者不知道如何处理的话,上报错误是一个选择。这种上报可能是一些语言中的throw exception ,也可能是直接函数返回错误, 简要来说就是停止接下来的处理流程,把如何处理交给上层。

      对于进程遇到的莫名错误来说,还有一种跟粗暴的方式,直接crash ,让重启来解决问题。这种方式,也被很多牛人推荐。

      在《Linux多线程服务端编程》中,陈硕大佬也有说明,对于分布式系统的软件来说,一般没必要要求7*24的可靠性,因为底层硬件的可靠性往往都达不到这种要求(对于大规模的分布式系统来说)。对于分布式系统来说,比起可靠性,更重要的可能是可用性,一台机器挂了,其他的机器可以立即顶上去。 所以在编程的时候,不必把大量的时间浪费在解决发生概率很小的错误上(内存分配失败、磁盘写满、mutex初始化失败)等,遇到这种情况时,可以直接退出进程。

二 、从不同编程语言看错误

2.1 C语言的处理方式

在这里插入图片描述

  对于C语言来说,最经典的处理方式就是通过error code + if… else … 方式来处理错误 了。 如下代码所示

int func(){
    
    
	int err = foo();
	if(err == errorCode){
    
    
		//....
	}else{
    
    
		//....
	}
}

  这种方式通过返回值error code来告知caller函数执行是否出错。 caller 通过if…else… 来对函数执行的结果进行判断,该正常执行还是进行出错处理(当然caller 也可以忽略不处理)。

  这种经典的错误处理方式,让人吐槽的最大的点在于这种方式会使得代码中充斥着大量无关的错误处理代码,使得业务逻辑和错误处理代码杂糅在一起

比如说下面的例子:

errorCodeType readFile
{
    
    
    //initialize errorCode = 0;
    //open the file;
    if (theFileIsOpen)
    {
    
    
        //determine the length of the file;
        if (gotTheFileLength)
        {
    
    
            //allocate that much memory;
            if (gotEnoughMemory)
            {
    
    
                //read the file into memory;
                if (readFailed)
                {
    
    
                    errorCode = -1;
                }
            }
            else
            {
    
    
                errorCode = -2;
            }
        }
        else
        {
    
    
            errorCode = -3;
        }
        //close the file;
        if (theFileDidntClose && errorCode == 0)
        {
    
    
            errorCode = -4;
        }
        else
        {
    
    
            errorCode = errorCode and -4;
        }
    }
    else 
    {
    
    
        errorCode = -5;
    }
    return errorCode;
}

啥都不说,我就问你头大不?

  不过这种方式也有一个好处,就是从某种方式上来说,强迫程序员处理错误。这在操作系统、存储、中间件领域等是比较重要的。

注:C语言也有setjmp 和 longjmp的机制,能够在一定程度上模拟cpp中的异常处理机制,这里就不说了。

2.2 CPP/JAVA的处理方式

在这里插入图片描述

  cpp和java对错误的处理方式是采用try… catch… 的方式。

如下代码所示:

void func() {
    
    
    throw exception; // 抛出异常
}

int main() {
    
    
    try {
    
     // try里放置可能抛出异常的代码,块中的代码被称为保护代码
        func();
    } catch (exception1& e) {
    
     // 捕获异常,异常类型为exception1
        // code
    } catch (exception2& e) {
    
     // 捕获异常,异常类型为exception2
        // code
    } catch (...) {
    
    
        // code
    }
    return 0;
}

  即出现错误时,服务的提供方(被调用者)throw 异常。 服务的调用者caller 利用try…catch… 方式捕获异常,进行处理(当然,也可以继续向上层throw)。

  《C++ 程序设计语言》 中用下面的的话来描述这种思想:

  1、当程序是由相互分离的模块组成时,库的作者可以检查出运行时错误,但是一般却不知道怎样去处理他们(抛出异常);用户的代码知道如何处理他们(捕获异常),却又无法检查它们(因为异常发生在库的代码中)

  2 将错误的产生和错误的处理分离(解耦)

  通过这种方式来达到避免业务代码中充斥着与业务逻辑无关的错误处理代码,使得代码结构清晰简洁的目的


  这种异常处理机制也有其缺点,一个明显的缺点就是性能上的问题。一般来说,使用了这种try…catch…的异常处理方式,其运行时和编译的时的效率都会有一定程度的下降。同时,使用异常和不使用异常比,二进制文件大小也会有约百分之十到二十的上升。【2】

  当然这里顺便说一句,C++,java实现的异常机制还有些不同,还有不少喷C++异常处理机制的(有些我也不是很理解) ̄□ ̄||。具体的可以参照【2】。

2.3 GO 中的处理方式

在这里插入图片描述

  对于go语言来说,它对erro的处理方式是 Errors are values 。设计者Rob Pike认为error和函数的其它返回值地位相等,只是多个返回值的其中之一,并无任何特殊之处。因此,对error的处理就如同正常对待一个函数的返回值一样进行。

如下所示为go中基本的错误处理方式:


result1,err := func1()
if err != nil{
    
    
    return nil,,err
}
result2,err := func2()
if err != nil{
    
    
    return nil,err
}
result3,err := func3()
if err != nil{
    
    
    return nil,err
}
return result1+result2+result3,nil

  是不是看着和C语言的那种处理方式有点熟悉。没错,作为互联网时代的C语言,go在设计的时候吸收了不少C中的设计思想。在错误处理方面,go强调让程序员更直接的接触、并且正视error

  同时,go语言中函数可以返回多个值的这个机制可以让 有潜在出错可能的函数 返回多个值, 比如说正常的result和 错误时的error ,这就基本解决了C语言中那种返回error code的同时还需要返回一个全局的errno。

  还有,由于go中的error是value,因此是可编程的。这就方便了程序员在处理错误时返回整个调用链上的调用堆栈信息(比如说可以在错误逐层传递时,层层都加日志)。

  当然,以上说的两点亮处,还是无法解决go语言中大量重复的 if err != nil 带来的割裂正常的业务逻辑问题。对于这个问题来说,业界已经有了一些技巧和方式,比如说Error Wrapping、pkg/errors以及Go2 Draft Design。 具体的可以参照【4】【5】, 这里就不多介绍了。

三、总结

  最后,以【5】中@大宽宽的回答作为一个总结吧。 对于“正确的处理错误” ?有两种不同的目标:

  (1)、要编写正确的代码。这里“正确”是指对任何设计上要处理的错误都必须设计处理流程。这类设计在操作系统、驱动、自动控制、存储、基础中间件等领域被使用;
  (2)、要尽量快速的开发质量可以接受的程序。此时可以忽略一大部分的错误的处理,就是放任。这种开发模式适用于RAD,如互联网领域里大量的上层应用和服务,网站,工具类的脚本等都隶属于这一类。

  对于前者可能共需要认真的设计每一条出错的路径,即使if…else…, 再多也无可厚非。而对于后者来说,把处理正确逻辑的路径重点完成。只处理少数的必要的错误,其余的一般会选择粗略的“集中处理”。

  现实中的项目往往处于这两种模式中间的某个点,靠某一边近一点而已。

  总的来说,代码写正确不容易,处理错误也不容易啊。


参考

【1】、C++异常机制:引用原因与使用原则
【2】、对使用 C++ 异常处理应具有怎样的态度?
【3】、C 语言异常处理
【4】、谈一谈Golang Error Handling
【5】、传统的try-catch异常处理是否是编程语言发展中的弯路?
【6】、从 C++ 的错误处理说起
【7】、你的c++团队还在禁用异常处理吗?
【8】、各种编程语言中的「错误/异常处理」有哪些成熟的,优雅的或是热门的机制/思想?
【9】、Try/catch Vs 返回错误类型,这两种异常处理的方式各有什么优缺点?
【10】、为什么不建议用 try catch?
【11】、Golang错误和异常处理的正确姿势
【12】、GO 编程模式:错误处理
【13】、深入 Go 的错误处理机制,理解设计思想
【14】、Go 语言的错误处理机制是一个优秀的设计吗?

猜你喜欢

转载自blog.csdn.net/plm199513100/article/details/120111516
今日推荐